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 000000000..ad5f4d6e9 Binary files /dev/null and b/tests/ref/issue-4087.png differ diff --git a/tests/ref/math-linebreaking-between-consecutive-relations.png b/tests/ref/math-linebreaking-between-consecutive-relations.png index ba222c573..7231456ad 100644 Binary files a/tests/ref/math-linebreaking-between-consecutive-relations.png and b/tests/ref/math-linebreaking-between-consecutive-relations.png differ diff --git a/tests/ref/trim-weak-space-line-beginning.png b/tests/ref/trim-weak-space-line-beginning.png new file mode 100644 index 000000000..37e137732 Binary files /dev/null and b/tests/ref/trim-weak-space-line-beginning.png differ diff --git a/tests/ref/trim-weak-space-line-end.png b/tests/ref/trim-weak-space-line-end.png new file mode 100644 index 000000000..004bb97aa Binary files /dev/null and b/tests/ref/trim-weak-space-line-end.png differ 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