Skip to content

Commit

Permalink
Fix hyphenation for some languages
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriel-araujjo committed May 2, 2024
1 parent a4d994d commit 2c35b93
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 8 deletions.
54 changes: 46 additions & 8 deletions crates/typst/src/layout/inline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -1458,3 +1482,17 @@ fn overhang(c: char) -> f64 {
_ => 0.0,
}
}

/// Whether the hyphen should repeat at the begin of line
fn should_repeat_hyphen(prev_line: &Line) -> bool {
match prev_line.dash {
Some(Dash::HardHyphen) => {
if let Some(Item::Text(shape)) = prev_line.last.as_ref() {
matches!(shape.lang, Lang::PORTUGUESE | Lang::POLISH)
} else {
false
}
}
_ => false,
}
}
50 changes: 50 additions & 0 deletions crates/typst/src/layout/inline/shaping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/typst/typst/issues/2283>.
.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<usize>) -> Option<&[ShapedGlyph]> {
Expand Down

0 comments on commit 2c35b93

Please sign in to comment.