diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs index b90c4e3b6..5bf2407bf 100644 --- a/crates/typst/src/model/outline.rs +++ b/crates/typst/src/model/outline.rs @@ -10,7 +10,9 @@ use crate::foundations::{ NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, }; use crate::introspection::{Counter, CounterKey, Locatable}; -use crate::layout::{BoxElem, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing}; +use crate::layout::{ + BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing, +}; use crate::model::{ Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable, }; @@ -499,12 +501,27 @@ impl Show for Packed { } }; - // The body text remains overridable. - crate::text::isolate( - self.body().clone().linked(Destination::Location(location)), - styles, - &mut seq, - ); + // Isolate the entry body in RTL because the page number is typically + // LTR. I'm not sure whether LTR should conceptually also be isolated, + // but in any case we don't do it for now because the text shaping + // pipeline does tend to choke a bit on default ignorables (in + // particular the CJK-Latin spacing). + // + // See also: + // - https://github.com/typst/typst/issues/4476 + // - https://github.com/typst/typst/issues/5176 + let rtl = TextElem::dir_in(styles) == Dir::RTL; + if rtl { + // "Right-to-Left Embedding" + seq.push(TextElem::packed("\u{202B}")); + } + + seq.push(self.body().clone().linked(Destination::Location(location))); + + if rtl { + // "Pop Directional Formatting" + seq.push(TextElem::packed("\u{202C}")); + } // Add filler symbols between the section name and page number. if let Some(filler) = self.fill() { diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs index 53163e186..5b35d60cd 100644 --- a/crates/typst/src/text/mod.rs +++ b/crates/typst/src/text/mod.rs @@ -1284,16 +1284,6 @@ pub(crate) fn is_default_ignorable(c: char) -> bool { DEFAULT_IGNORABLE_DATA.as_borrowed().contains(c) } -/// Pushes `text` wrapped in LRE/RLE + PDF to `out`. -pub(crate) fn isolate(text: Content, styles: StyleChain, out: &mut Vec) { - out.push(TextElem::packed(match TextElem::dir_in(styles) { - Dir::RTL => "\u{202B}", - _ => "\u{202A}", - })); - out.push(text); - out.push(TextElem::packed("\u{202C}")); -} - /// Checks for font families that are not available. fn check_font_list(engine: &mut Engine, list: &Spanned) { let book = engine.world.book(); diff --git a/tests/ref/issue-5176-cjk-title.png b/tests/ref/issue-5176-cjk-title.png new file mode 100644 index 000000000..e904fbd77 Binary files /dev/null and b/tests/ref/issue-5176-cjk-title.png differ diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ index 3c134760c..18c61df8a 100644 --- a/tests/suite/model/outline.typ +++ b/tests/suite/model/outline.typ @@ -171,3 +171,12 @@ A = הוקוס Pocus = זוהי כותרת שתורגמה על ידי מחשב + +--- issue-5176-cjk-title --- +#set text(font: "Noto Serif CJK SC") +#show heading: none + +#outline(title: none) + += 测 += 很