From 008eaf078cc3acb9f9651f02dbda31c25cbb04c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ara=C3=BAjo?= Date: Thu, 2 May 2024 00:35:40 -0300 Subject: [PATCH] Fix hyphenation for some languages - Czech - Croatian - Lower Sorbian - Polish - Portuguese - Slovak - Spanish Fix #3235 --- crates/typst/src/layout/inline/mod.rs | 87 ++++++++++++++++-- crates/typst/src/layout/inline/shaping.rs | 50 ++++++++++ tests/ref/hyphenate-es-captalized-names.png | Bin 0 -> 4346 bytes tests/ref/hyphenate-es-repeat-hyphen.png | Bin 0 -> 3410 bytes tests/ref/hyphenate-pt-dash-emphasis.png | Bin 0 -> 1036 bytes tests/ref/hyphenate-pt-no-repeat-hyphen.png | Bin 0 -> 1629 bytes ...henate-pt-repeat-hyphen-hyphenate-true.png | Bin 0 -> 1414 bytes ...pt-repeat-hyphen-natural-word-breaking.png | Bin 0 -> 1414 bytes tests/suite/layout/inline/hyphenate.typ | 46 +++++++++ 9 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 tests/ref/hyphenate-es-captalized-names.png create mode 100644 tests/ref/hyphenate-es-repeat-hyphen.png create mode 100644 tests/ref/hyphenate-pt-dash-emphasis.png create mode 100644 tests/ref/hyphenate-pt-no-repeat-hyphen.png create mode 100644 tests/ref/hyphenate-pt-repeat-hyphen-hyphenate-true.png create mode 100644 tests/ref/hyphenate-pt-repeat-hyphen-natural-word-breaking.png diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index 271a8a92b0c8..6897837b0ba6 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -816,8 +816,10 @@ fn linebreak_simple<'a>( let mut last = None; breakpoints(p, |end, breakpoint| { + let prepend_hyphen = lines.last().map(should_repeat_hyphen).unwrap_or(false); + // Compute the line and its size. - let mut attempt = line(engine, p, start..end, breakpoint); + let mut attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); // If the line doesn't fit anymore, we push the last fitting attempt // into the stack and rebuild the line from the attempt's end. The @@ -826,7 +828,7 @@ fn linebreak_simple<'a>( if let Some((last_attempt, last_end)) = last.take() { lines.push(last_attempt); start = last_end; - attempt = line(engine, p, start..end, breakpoint); + attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); } } @@ -896,7 +898,7 @@ fn linebreak_optimized<'a>( let mut table = vec![Entry { pred: 0, total: 0.0, - line: line(engine, p, 0..0, Breakpoint::Mandatory), + line: line(engine, p, 0..0, Breakpoint::Mandatory, false), }]; let em = p.size; @@ -910,8 +912,9 @@ fn linebreak_optimized<'a>( for (i, pred) in table.iter().enumerate().skip(active) { // Layout the line. let start = pred.line.end; + let prepend_hyphen = should_repeat_hyphen(&pred.line); - let attempt = line(engine, p, start..end, breakpoint); + let attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); // Determine how much the line's spaces would need to be stretched // to make it the desired width. @@ -1024,6 +1027,7 @@ fn line<'a>( p: &'a Preparation, mut range: Range, breakpoint: Breakpoint, + prepend_hyphen: bool, ) -> Line<'a> { let end = range.end; let mut justify = @@ -1091,13 +1095,25 @@ fn line<'a>( // need the shaped empty string to make the line the appropriate // height. That is the case exactly if the string is empty and there // are no other items in the line. - if hyphen || start + shaped.text.len() > range.end || maybe_adjust_last_glyph { - if hyphen || start < range.end || before.is_empty() { + if hyphen + || start + shaped.text.len() > range.end + || maybe_adjust_last_glyph + || (prepend_hyphen && before.is_empty()) + { + if hyphen + || start < range.end + || before.is_empty() + || (prepend_hyphen && before.is_empty()) + { let mut reshaped = shaped.reshape(engine, &p.spans, start..range.end); if hyphen || shy { reshaped.push_hyphen(engine, p.fallback); } + if prepend_hyphen && before.is_empty() { + reshaped.prepend_hyphen(engine, p.fallback); + } + if let Some(last_glyph) = reshaped.glyphs.last() { if last_glyph.is_cjk_left_aligned_punctuation(gb_style) { // If the last glyph is a CJK punctuation, we want to shrink it. @@ -1143,10 +1159,18 @@ fn line<'a>( let end = range.end.min(base + shaped.text.len()); // Reshape if necessary. - if range.start + shaped.text.len() > end || maybe_adjust_first_glyph { + if range.start + shaped.text.len() > end + || maybe_adjust_first_glyph + || prepend_hyphen + { // If the range is empty, we don't want to push an empty text item. if range.start < end { - let reshaped = shaped.reshape(engine, &p.spans, range.start..end); + let mut reshaped = shaped.reshape(engine, &p.spans, range.start..end); + + if prepend_hyphen { + reshaped.prepend_hyphen(engine, p.fallback) + } + width += reshaped.width; first = Some(Item::Text(reshaped)); } @@ -1458,3 +1482,50 @@ fn overhang(c: char) -> f64 { _ => 0.0, } } + +/// Whether the hyphen should repeat at the begin of the next line +fn should_repeat_hyphen(pred_line: &Line) -> bool { + // If the predecessor line does not end with a Dash::HardHyphen, we shall not place a hyphen at + // the beginning of the next line. + if pred_line.dash != Some(Dash::HardHyphen) { + return false; + } + + // If there's a trimmed out space, we needn't repeat the hyphen. That's the case of a text like + // "... kebab é a -melhor- comida que existe", where the hyphens are a kind of emphasis marker. + if pred_line.trimmed.end != pred_line.end { + return false; + } + + // The hyphen should repeat only in the languages that requires that feature. + // For more information see the discussion at https://github.com/typst/typst/issues/3235 + if let Some(Item::Text(shape)) = pred_line.last.as_ref() { + match shape.lang.as_str() { + // Lower Sorbian: see https://dolnoserbski.de/ortografija/psawidla/K3 + // + // Czech: see https://prirucka.ujc.cas.cz/?id=164 + // + // Croatian: see http://pravopis.hr/pravilo/spojnica/68/ + // + // Polish: see https://www.ortograf.pl/zasady-pisowni/lacznik-zasady-pisowni + // + // Portuguese: see Base XX of "Acordo Ortográfico da Língua Portuguesa de 1990" + // https://www2.senado.leg.br/bdsf/bitstream/handle/id/508145/000997415.pdf + // + // Slovak: see https://www.zones.sk/studentske-prace/gramatika/10620-pravopis-rozdelovanie-slov/ + "dsb" | "cs" | "hr" | "pl" | "pt" | "sk" => true, + // In Spanish the hyphen is required only if the word next to hyphen isn't capitalized. + // + // See § 4.1.1.1.2.e on the "Ortografía de la lengua española" + // https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea + "es" => pred_line.bidi.text[pred_line.end..] + .chars() + .next() + .map(|c| !c.is_uppercase()) + .unwrap_or(false), + _ => false, + } + } else { + false + } +} diff --git a/crates/typst/src/layout/inline/shaping.rs b/crates/typst/src/layout/inline/shaping.rs index 82a33f9b09a3..0517d9dc1f4b 100644 --- a/crates/typst/src/layout/inline/shaping.rs +++ b/crates/typst/src/layout/inline/shaping.rs @@ -492,6 +492,56 @@ impl<'a> ShapedText<'a> { }); } + /// Prepend a hyphen to begin of the text. + pub fn prepend_hyphen(&mut self, engine: &Engine, fallback: bool) { + let world = engine.world; + let book = world.book(); + let fallback_func = if fallback { + Some(|| book.select_fallback(None, self.variant, "-")) + } else { + None + }; + let mut chain = families(self.styles) + .map(|family| book.select(family, self.variant)) + .chain(fallback_func.iter().map(|f| f())) + .flatten(); + + chain.find_map(|id| { + let font = world.font(id)?; + let ttf = font.ttf(); + let glyph_id = ttf.glyph_index('-')?; + let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?); + let range = self + .glyphs + .first() + .map(|g| g.range.start..g.range.start) + // In the unlikely chance that we hyphenate after an empty line, + // ensure that the glyph range still falls after self.base so + // that subtracting either of the endpoints by self.base doesn't + // underflow. See . + .unwrap_or_else(|| self.base..self.base); + self.width += x_advance.at(self.size); + self.glyphs.to_mut().insert( + 0, + ShapedGlyph { + font, + glyph_id: glyph_id.0, + x_advance, + x_offset: Em::zero(), + y_offset: Em::zero(), + adjustability: Adjustability::default(), + range, + safe_to_break: true, + c: '-', + span: (Span::detached(), 0), + is_justifiable: false, + script: Script::Common, + }, + ); + Some(()) + }); + } + /// Find the subslice of glyphs that represent the given text range if both /// sides are safe to break. fn slice_safe_to_break(&self, text_range: Range) -> Option<&[ShapedGlyph]> { diff --git a/tests/ref/hyphenate-es-captalized-names.png b/tests/ref/hyphenate-es-captalized-names.png new file mode 100644 index 0000000000000000000000000000000000000000..a11ed409b8fa126e8d12d1ef435147fee51d6929 GIT binary patch literal 4346 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 literal 0 HcmV?d00001 diff --git a/tests/ref/hyphenate-es-repeat-hyphen.png b/tests/ref/hyphenate-es-repeat-hyphen.png new file mode 100644 index 0000000000000000000000000000000000000000..7220e72a35ee0d3ce70875697dcf1365029ac214 GIT binary patch literal 3410 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 bf295orr2;sGfB)!00000NkvXXu0mjftS%ys literal 0 HcmV?d00001 diff --git a/tests/ref/hyphenate-pt-repeat-hyphen-hyphenate-true.png b/tests/ref/hyphenate-pt-repeat-hyphen-hyphenate-true.png new file mode 100644 index 0000000000000000000000000000000000000000..0cb9df556272363b4bb6d587e0b81fe6aaa3eccf GIT binary patch literal 1414 zcmV;11$p|3P)5ihPdfercCkc2;4HY8@wK z%%W}`r6o4iV3~lvKXkgwyWKhdFDT4weC< zi)wiWq{;JBQ(FX~{u|LHc4cVg87g^cp1iRjzq$a>Cn}&Mww+9Q-GiuR6pE@s(8x2J z^!ZKb=IH&d#~CcU`kxj%yz+Rlnqzf;p5>pV z!B8=$;LXeK9Yw?~j+a{fto|GLyPHxNs&xTxR!0_89c2_z?-dUwMz-a-dIcsJ{?GtJ zSKmT_s%9_(fRWKMAa??->19#xES?pw5e8lb@cB%4*17HHP}c#`TKt^^U)dpmKd$XA z2K=M;4T$N%hlRxtJjLDs&f*g9cA9_me&TW5DBqORQM<-RrRs--9$h_XHl*S}U zYZ9al$!YlmVn5ihPdfercCkc2;4HY8@wK z%%W}`r6o4iV3~lvKXkgwyWKhdFDT4weC< zi)wiWq{;JBQ(FX~{u|LHc4cVg87g^cp1iRjzq$a>Cn}&Mww+9Q-GiuR6pE@s(8x2J z^!ZKb=IH&d#~CcU`kxj%yz+Rlnqzf;p5>pV z!B8=$;LXeK9Yw?~j+a{fto|GLyPHxNs&xTxR!0_89c2_z?-dUwMz-a-dIcsJ{?GtJ zSKmT_s%9_(fRWKMAa??->19#xES?pw5e8lb@cB%4*17HHP}c#`TKt^^U)dpmKd$XA z2K=M;4T$N%hlRxtJjLDs&f*g9cA9_me&TW5DBqORQM<-RrRs--9$h_XHl*S}U zYZ9al$!YlmVn