From b73b3ca33521a4f4546e14b788a8de227602f947 Mon Sep 17 00:00:00 2001 From: LU Jialin Date: Mon, 27 May 2024 08:36:41 -0700 Subject: [PATCH] Trim weak spacing at line start/end in paragraph layout (#4087) --- crates/typst/src/layout/inline/mod.rs | 44 ++++++++++++------ tests/ref/issue-4087.png | Bin 0 -> 2681 bytes ...breaking-between-consecutive-relations.png | Bin 387 -> 387 bytes tests/ref/trim-weak-space-line-beginning.png | Bin 0 -> 245 bytes tests/ref/trim-weak-space-line-end.png | Bin 0 -> 246 bytes tests/suite/layout/spacing.typ | 22 +++++++++ tests/suite/math/multiline.typ | 5 +- 7 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 tests/ref/issue-4087.png create mode 100644 tests/ref/trim-weak-space-line-beginning.png create mode 100644 tests/ref/trim-weak-space-line-end.png diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index 6a39537c0..507f12355 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -195,8 +195,8 @@ enum Segment<'a> { /// One or multiple collapsed text or text-equivalent children. Stores how /// long the segment is (in bytes of the full text string). Text(usize), - /// Horizontal spacing between other segments. - Spacing(Spacing), + /// Horizontal spacing between other segments. Bool when true indicate weak space + Spacing(Spacing, bool), /// A mathematical equation. Equation(Vec), /// A box with arbitrary content. @@ -210,7 +210,7 @@ impl Segment<'_> { fn len(&self) -> usize { match *self { Self::Text(len) => len, - Self::Spacing(_) => SPACING_REPLACE.len_utf8(), + Self::Spacing(_, _) => SPACING_REPLACE.len_utf8(), Self::Box(_, frac) => { (if frac { SPACING_REPLACE } else { OBJ_REPLACE }).len_utf8() } @@ -231,7 +231,7 @@ enum Item<'a> { /// A shaped text run with consistent style and direction. Text(ShapedText<'a>), /// Absolute spacing between other items. - Absolute(Abs), + Absolute(Abs, bool), /// Fractional spacing between other items. Fractional(Fr, Option<(&'a Packed, StyleChain<'a>)>), /// Layouted inline-level content. @@ -264,7 +264,7 @@ impl<'a> Item<'a> { fn len(&self) -> usize { match self { Self::Text(shaped) => shaped.text.len(), - Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(), + Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(), Self::Frame(_) => OBJ_REPLACE.len_utf8(), Self::Tag(_) => 0, Self::Skip(c) => c.len_utf8(), @@ -275,7 +275,7 @@ impl<'a> Item<'a> { fn width(&self) -> Abs { match self { Self::Text(shaped) => shaped.width, - Self::Absolute(v) => *v, + Self::Absolute(v, _) => *v, Self::Frame(frame) => frame.width(), Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(), Self::Skip(_) => Abs::zero(), @@ -453,13 +453,13 @@ fn collect<'a>( == TextElem::dir_in(*styles).start().into() { full.push(SPACING_REPLACE); - segments.push((Segment::Spacing(first_line_indent.into()), *styles)); + segments.push((Segment::Spacing(first_line_indent.into(), false), *styles)); } let hang = ParElem::hanging_indent_in(*styles); if !hang.is_zero() { full.push(SPACING_REPLACE); - segments.push((Segment::Spacing((-hang).into()), *styles)); + segments.push((Segment::Spacing((-hang).into(), false), *styles)); } let outer_dir = TextElem::dir_in(*styles); @@ -504,7 +504,7 @@ fn collect<'a>( } full.push(SPACING_REPLACE); - Segment::Spacing(*elem.amount()) + Segment::Spacing(*elem.amount(), elem.weak(styles)) } else if let Some(elem) = child.to_packed::() { let c = if elem.justify(styles) { '\u{2028}' } else { '\n' }; full.push(c); @@ -618,10 +618,10 @@ fn prepare<'a>( Segment::Text(_) => { shape_range(&mut items, engine, &bidi, cursor..end, &spans, styles); } - Segment::Spacing(spacing) => match spacing { + Segment::Spacing(spacing, weak) => match spacing { Spacing::Rel(v) => { let resolved = v.resolve(styles).relative_to(region.x); - items.push(Item::Absolute(resolved)); + items.push(Item::Absolute(resolved, weak)); } Spacing::Fr(v) => { items.push(Item::Fractional(v, None)); @@ -631,7 +631,8 @@ fn prepare<'a>( items.push(Item::Skip(LTR_ISOLATE)); for item in par_items { match item { - MathParItem::Space(s) => items.push(Item::Absolute(s)), + // MathParItem space are assumed to be weak space + MathParItem::Space(s) => items.push(Item::Absolute(s, true)), MathParItem::Frame(mut frame) => { frame.translate(Point::with_y(TextElem::baseline_in(styles))); items.push(Item::Frame(frame)); @@ -1079,9 +1080,24 @@ fn line<'a>( } // Slice out the relevant items. - let (expanded, mut inner) = p.slice(range.clone()); + let (mut expanded, mut inner) = p.slice(range.clone()); let mut width = Abs::zero(); + // Weak space (Absolute(_, weak=true)) would be removed if at the end of the line + while let Some((Item::Absolute(_, true), before)) = inner.split_last() { + // apply it recursively to ensure the last one is not weak space + inner = before; + range.end -= 1; + expanded.end -= 1; + } + // Weak space (Absolute(_, weak=true)) would be removed if at the beginning of the line + while let Some((Item::Absolute(_, true), after)) = inner.split_first() { + // apply it recursively to ensure the first one is not weak space + inner = after; + range.start += 1; + expanded.end += 1; + } + // Reshape the last item if it's split in half or hyphenated. let mut last = None; let mut dash = None; @@ -1402,7 +1418,7 @@ fn commit( }; match item { - Item::Absolute(v) => { + Item::Absolute(v, _) => { offset += *v; } Item::Fractional(v, elem) => { diff --git a/tests/ref/issue-4087.png b/tests/ref/issue-4087.png new file mode 100644 index 0000000000000000000000000000000000000000..ad5f4d6e9e8e5bc1f514bb06327451ea168388b4 GIT binary patch literal 2681 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 literal 0 HcmV?d00001 diff --git a/tests/ref/math-linebreaking-between-consecutive-relations.png b/tests/ref/math-linebreaking-between-consecutive-relations.png index ba222c5739d67d35e9ed0073d24643dc995d172e..7231456ad6ee9fcbed05b93c5d3f0e8d25569a21 100644 GIT binary patch delta 44 zcmZo>Zf2fvgO^n$qGs-@1d~2@UAZR{pLU7pzO%}obNHtj0}yz+`njxgN@xNAd)^SX delta 44 ycmZo>Zf2fvgO^z)s%CChf=Qpdt{l_Er(GiZ&e~6*6+WSpE=w6HH- zAL(WBZ|$#NzuG_lzj=8N#Nxk`7WC7_V%dcY7ivAf9CC`T66V?*H~L7FGVAv4IX2&(7KY|3F^<|5fFy wzc<#MC|LS`_UxsjCD*9MqZW@^JftlK0GkVO>Qm7onE(I)07*qoM6N<$f~ebfZ2$lO literal 0 HcmV?d00001 diff --git a/tests/suite/layout/spacing.typ b/tests/suite/layout/spacing.typ index 430e97794..dd0fced55 100644 --- a/tests/suite/layout/spacing.typ +++ b/tests/suite/layout/spacing.typ @@ -36,3 +36,25 @@ Totally #h() ignored [Hello ] counter(heading).display() } + +--- trim-weak-space-line-beginning --- +// Weak space at the beginning should be removed. +#h(2cm, weak: true) Hello + +--- trim-weak-space-line-end --- +// Weak space at the end of the line should be removed. +#set align(right) +Hello #h(2cm, weak: true) + +--- issue-4087 --- +// weak space at the end of the line would be removed. +This is the first line #h(2cm, weak: true) A new line + +// non-weak space would be consume a specified width and push next line. +This is the first line #h(2cm, weak: false) A new line + +// similarly weak space at the beginning of the line would be removed. +This is the first line\ #h(2cm, weak: true) A new line + +// non-spacing, on the other hand, is not removed. +This is the first line\ #h(2cm, weak: false) A new line diff --git a/tests/suite/math/multiline.typ b/tests/suite/math/multiline.typ index 85433627f..edf974a10 100644 --- a/tests/suite/math/multiline.typ +++ b/tests/suite/math/multiline.typ @@ -86,9 +86,12 @@ Multiple trailing line breaks. --- math-linebreaking-between-consecutive-relations --- // A relation followed by a relation doesn't linebreak +// so essentially `a < = b` can be broken to `a` and `< = b`, `a < =` and `b` +// but never `a <` and `= b` because `< =` are consecutive relation that should +// be grouped together and no break between them. #let hrule(x) = box(line(length: x)) #hrule(70pt)$a < = b$\ -#hrule(74pt)$a < = b$ +#hrule(78pt)$a < = b$ --- math-linebreaking-after-relation-without-space --- // Line breaks can happen after a relation even if there is no