Skip to content

Commit 09f222b

Browse files
authored
Merge pull request #1641 from cN3rd/rtl
Continued work on "Support right-to-left languages"
2 parents 25aaff0 + 802e7bf commit 09f222b

File tree

9 files changed

+251
-63
lines changed

9 files changed

+251
-63
lines changed

guide/src/format/configuration/general.md

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ This is general information about your book.
4646
`src` directly under the root folder. But this is configurable with the `src`
4747
key in the configuration file.
4848
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
49+
This is also used to derive the direction of text (RTL, LTR) within the book.
50+
- **text_direction**: The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). Possible values: `ltr`, `rtl`.
51+
When not specified, the text direction is derived from the book's `language` attribute.
4952

5053
**book.toml**
5154
```toml
@@ -55,6 +58,7 @@ authors = ["John Doe", "Jane Doe"]
5558
description = "The example book covers examples."
5659
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
5760
language = "en"
61+
text-direction = "ltr"
5862
```
5963

6064
### Rust options

src/config.rs

+108
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ pub struct BookConfig {
411411
pub multilingual: bool,
412412
/// The main language of the book.
413413
pub language: Option<String>,
414+
/// The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL).
415+
/// When not specified, the text direction is derived from [`BookConfig::language`].
416+
pub text_direction: Option<TextDirection>,
414417
}
415418

416419
impl Default for BookConfig {
@@ -422,6 +425,43 @@ impl Default for BookConfig {
422425
src: PathBuf::from("src"),
423426
multilingual: false,
424427
language: Some(String::from("en")),
428+
text_direction: None,
429+
}
430+
}
431+
}
432+
433+
impl BookConfig {
434+
/// Gets the realized text direction, either from [`BookConfig::text_direction`]
435+
/// or derived from [`BookConfig::language`], to be used by templating engines.
436+
pub fn realized_text_direction(&self) -> TextDirection {
437+
if let Some(direction) = self.text_direction {
438+
direction
439+
} else {
440+
TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default())
441+
}
442+
}
443+
}
444+
445+
/// Text direction to use for HTML output
446+
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
447+
pub enum TextDirection {
448+
/// Left to right.
449+
#[serde(rename = "ltr")]
450+
LeftToRight,
451+
/// Right to left
452+
#[serde(rename = "rtl")]
453+
RightToLeft,
454+
}
455+
456+
impl TextDirection {
457+
/// Gets the text direction from language code
458+
pub fn from_lang_code(code: &str) -> Self {
459+
match code {
460+
// list sourced from here: https://github.com/abarrak/rtl/blob/master/lib/rtl/core.rb#L16
461+
"ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn"
462+
| "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd"
463+
| "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft,
464+
_ => TextDirection::LeftToRight,
425465
}
426466
}
427467
}
@@ -788,6 +828,7 @@ mod tests {
788828
multilingual: true,
789829
src: PathBuf::from("source"),
790830
language: Some(String::from("ja")),
831+
text_direction: None,
791832
};
792833
let build_should_be = BuildConfig {
793834
build_dir: PathBuf::from("outputs"),
@@ -1140,6 +1181,73 @@ mod tests {
11401181
assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
11411182
}
11421183

1184+
#[test]
1185+
fn text_direction_ltr() {
1186+
let src = r#"
1187+
[book]
1188+
text-direction = "ltr"
1189+
"#;
1190+
1191+
let got = Config::from_str(src).unwrap();
1192+
assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight));
1193+
}
1194+
1195+
#[test]
1196+
fn text_direction_rtl() {
1197+
let src = r#"
1198+
[book]
1199+
text-direction = "rtl"
1200+
"#;
1201+
1202+
let got = Config::from_str(src).unwrap();
1203+
assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft));
1204+
}
1205+
1206+
#[test]
1207+
fn text_direction_none() {
1208+
let src = r#"
1209+
[book]
1210+
"#;
1211+
1212+
let got = Config::from_str(src).unwrap();
1213+
assert_eq!(got.book.text_direction, None);
1214+
}
1215+
1216+
#[test]
1217+
fn test_text_direction() {
1218+
let mut cfg = BookConfig::default();
1219+
1220+
// test deriving the text direction from language codes
1221+
cfg.language = Some("ar".into());
1222+
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1223+
1224+
cfg.language = Some("he".into());
1225+
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1226+
1227+
cfg.language = Some("en".into());
1228+
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1229+
1230+
cfg.language = Some("ja".into());
1231+
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1232+
1233+
// test forced direction
1234+
cfg.language = Some("ar".into());
1235+
cfg.text_direction = Some(TextDirection::LeftToRight);
1236+
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1237+
1238+
cfg.language = Some("ar".into());
1239+
cfg.text_direction = Some(TextDirection::RightToLeft);
1240+
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1241+
1242+
cfg.language = Some("en".into());
1243+
cfg.text_direction = Some(TextDirection::LeftToRight);
1244+
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
1245+
1246+
cfg.language = Some("en".into());
1247+
cfg.text_direction = Some(TextDirection::RightToLeft);
1248+
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
1249+
}
1250+
11431251
#[test]
11441252
#[should_panic(expected = "Invalid configuration file")]
11451253
fn invalid_language_type_error() {

src/renderer/html_handlebars/hbs_renderer.rs

+12
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,10 @@ fn make_data(
648648
"language".to_owned(),
649649
json!(config.book.language.clone().unwrap_or_default()),
650650
);
651+
data.insert(
652+
"text_direction".to_owned(),
653+
json!(config.book.realized_text_direction()),
654+
);
651655
data.insert(
652656
"book_title".to_owned(),
653657
json!(config.book.title.clone().unwrap_or_default()),
@@ -1088,6 +1092,8 @@ struct RenderItemContext<'a> {
10881092

10891093
#[cfg(test)]
10901094
mod tests {
1095+
use crate::config::TextDirection;
1096+
10911097
use super::*;
10921098
use pretty_assertions::assert_eq;
10931099

@@ -1299,4 +1305,10 @@ mod tests {
12991305
assert_eq!(&*got, *should_be);
13001306
}
13011307
}
1308+
1309+
#[test]
1310+
fn test_json_direction() {
1311+
assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
1312+
assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
1313+
}
13021314
}

src/theme/book.js

+31-16
Original file line numberDiff line numberDiff line change
@@ -441,16 +441,16 @@ function playground_text(playground, hidden = true) {
441441
})();
442442

443443
(function sidebar() {
444-
var html = document.querySelector("html");
444+
var body = document.querySelector("body");
445445
var sidebar = document.getElementById("sidebar");
446446
var sidebarLinks = document.querySelectorAll('#sidebar a');
447447
var sidebarToggleButton = document.getElementById("sidebar-toggle");
448448
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
449449
var firstContact = null;
450450

451451
function showSidebar() {
452-
html.classList.remove('sidebar-hidden')
453-
html.classList.add('sidebar-visible');
452+
body.classList.remove('sidebar-hidden')
453+
body.classList.add('sidebar-visible');
454454
Array.from(sidebarLinks).forEach(function (link) {
455455
link.setAttribute('tabIndex', 0);
456456
});
@@ -471,8 +471,8 @@ function playground_text(playground, hidden = true) {
471471
});
472472

473473
function hideSidebar() {
474-
html.classList.remove('sidebar-visible')
475-
html.classList.add('sidebar-hidden');
474+
body.classList.remove('sidebar-visible')
475+
body.classList.add('sidebar-hidden');
476476
Array.from(sidebarLinks).forEach(function (link) {
477477
link.setAttribute('tabIndex', -1);
478478
});
@@ -483,14 +483,14 @@ function playground_text(playground, hidden = true) {
483483

484484
// Toggle sidebar
485485
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
486-
if (html.classList.contains("sidebar-hidden")) {
486+
if (body.classList.contains("sidebar-hidden")) {
487487
var current_width = parseInt(
488488
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
489489
if (current_width < 150) {
490490
document.documentElement.style.setProperty('--sidebar-width', '150px');
491491
}
492492
showSidebar();
493-
} else if (html.classList.contains("sidebar-visible")) {
493+
} else if (body.classList.contains("sidebar-visible")) {
494494
hideSidebar();
495495
} else {
496496
if (getComputedStyle(sidebar)['transform'] === 'none') {
@@ -506,14 +506,14 @@ function playground_text(playground, hidden = true) {
506506
function initResize(e) {
507507
window.addEventListener('mousemove', resize, false);
508508
window.addEventListener('mouseup', stopResize, false);
509-
html.classList.add('sidebar-resizing');
509+
body.classList.add('sidebar-resizing');
510510
}
511511
function resize(e) {
512512
var pos = (e.clientX - sidebar.offsetLeft);
513513
if (pos < 20) {
514514
hideSidebar();
515515
} else {
516-
if (html.classList.contains("sidebar-hidden")) {
516+
if (body.classList.contains("sidebar-hidden")) {
517517
showSidebar();
518518
}
519519
pos = Math.min(pos, window.innerWidth - 100);
@@ -522,7 +522,7 @@ function playground_text(playground, hidden = true) {
522522
}
523523
//on mouseup remove windows functions mousemove & mouseup
524524
function stopResize(e) {
525-
html.classList.remove('sidebar-resizing');
525+
body.classList.remove('sidebar-resizing');
526526
window.removeEventListener('mousemove', resize, false);
527527
window.removeEventListener('mouseup', stopResize, false);
528528
}
@@ -557,20 +557,35 @@ function playground_text(playground, hidden = true) {
557557
document.addEventListener('keydown', function (e) {
558558
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
559559
if (window.search && window.search.hasFocus()) { return; }
560+
var html = document.querySelector('html');
560561

562+
function next() {
563+
var nextButton = document.querySelector('.nav-chapters.next');
564+
if (nextButton) {
565+
window.location.href = nextButton.href;
566+
}
567+
}
568+
function prev() {
569+
var previousButton = document.querySelector('.nav-chapters.previous');
570+
if (previousButton) {
571+
window.location.href = previousButton.href;
572+
}
573+
}
561574
switch (e.key) {
562575
case 'ArrowRight':
563576
e.preventDefault();
564-
var nextButton = document.querySelector('.nav-chapters.next');
565-
if (nextButton) {
566-
window.location.href = nextButton.href;
577+
if (html.dir == 'rtl') {
578+
prev();
579+
} else {
580+
next();
567581
}
568582
break;
569583
case 'ArrowLeft':
570584
e.preventDefault();
571-
var previousButton = document.querySelector('.nav-chapters.previous');
572-
if (previousButton) {
573-
window.location.href = previousButton.href;
585+
if (html.dir == 'rtl') {
586+
next();
587+
} else {
588+
prev();
574589
}
575590
break;
576591
}

0 commit comments

Comments
 (0)