diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs index f7cf90bd6..05ed4633e 100644 --- a/crates/typst-library/src/text/shaping.rs +++ b/crates/typst-library/src/text/shaping.rs @@ -488,17 +488,35 @@ impl<'a> ShapedText<'a> { } // Find any glyph with the text index. - let mut idx = self - .glyphs - .binary_search_by(|g| { - let ordering = g.range.start.cmp(&text_index); - if ltr { - ordering - } else { - ordering.reverse() - } - }) - .ok()?; + let found = self.glyphs.binary_search_by(|g: &ShapedGlyph| { + let ordering = g.range.start.cmp(&text_index); + if ltr { + ordering + } else { + ordering.reverse() + } + }); + let mut idx = match found { + Ok(idx) => idx, + Err(idx) => { + // Handle the special case where we break before a '\n' + // + // For example: (assume `a` is a CJK character with three bytes) + // text: " a \n b " + // index: 0 1 2 3 4 5 + // text_index: ^ + // glyphs: 0 . 1 + // + // We will get found = Err(1), because '\n' does not have a glyph. + // But it's safe to break here. Thus the following condition: + // - glyphs[0].end == text_index == 3 + // - text[3] == '\n' + return (idx > 0 + && self.glyphs[idx - 1].range.end == text_index + && self.text[text_index - self.base..].starts_with('\n')) + .then_some(idx); + } + }; let next = match towards { Side::Left => usize::checked_sub, diff --git a/tests/ref/layout/cjk-latin-spacing.png b/tests/ref/layout/cjk-latin-spacing.png index bd4eed9b4..629145e46 100644 Binary files a/tests/ref/layout/cjk-latin-spacing.png and b/tests/ref/layout/cjk-latin-spacing.png differ diff --git a/tests/typ/layout/cjk-latin-spacing.typ b/tests/typ/layout/cjk-latin-spacing.typ index 9cc94fd23..c6fff5d79 100644 --- a/tests/typ/layout/cjk-latin-spacing.typ +++ b/tests/typ/layout/cjk-latin-spacing.typ @@ -14,3 +14,16 @@ 中文,中ab文a中,文ab中文 +--- +// Issue #2538 +#set text(cjk-latin-spacing: auto) + +abc字 + +abc字#linebreak() + +abc字#linebreak() +母 + +abc字\ +母