Repository: typst/typst
File: crates/typst/src/layout/inline/mod.rs (Rust)

Code before:
        // Usually, we don't want to shape an empty string because:
        // - We don't want the height of trimmed whitespace in a different
        //   font to be considered for the line height.
        // - Even if it's in the same font, its unnecessary.
        //
        // There is one exception though. When the whole line is empty, we
        // 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() {
                let mut reshaped = shaped.reshape(engine, &p.spans, start..range.end);
                if hyphen || shy {
                    reshaped.push_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.
                        // See Requirements for Chinese Text Layout, Section 3.1.6.3
                        // Compression of punctuation marks at line start or line end
                        let shrink_amount = last_glyph.shrinkability().1;
                        let punct = reshaped.glyphs.to_mut().last_mut().unwrap();
                        // No need to add `stretchability` of the last CJK punctuation glyph to avoid stretching in line adjustment.
                        punct.x_advance -= shrink_amount;
                        punct.adjustability.shrinkability.1 -= shrink_amount;
                        reshaped.width -= shrink_amount.at(reshaped.size);
                    } else if p.cjk_latin_spacing
                        && last_glyph.is_cj_script()
                        && (last_glyph.x_advance - last_glyph.x_offset) > Em::one()
                    {
                        // If the last glyph is a CJK character adjusted by [`add_cjk_latin_spacing`],
                        // restore the original width.
                        let shrink_amount =
                            last_glyph.x_advance - last_glyph.x_offset - Em::one();
                        let glyph = reshaped.glyphs.to_mut().last_mut().unwrap();
                        glyph.x_advance -= shrink_amount;
                        glyph.adjustability.shrinkability.1 = Em::zero();
                        reshaped.width -= shrink_amount.at(reshaped.size);
                    }
                }

                width += reshaped.width;
                last = Some(Item::Text(reshaped));
            }

            inner = before;
        }
    }

    // Deal with CJ characters at line starts.

Review comment (suggestion):
There are three places of adjustments:

- At line start;
- At line end;
- Between two consecutive punctuation marks.

All of the three places shouldn't add the stretchability. So maybe you can just change the implementation of `shrink_right` and `shrink_left`.

Code after:
        // Usually, we don't want to shape an empty string because:
        // - We don't want the height of trimmed whitespace in a different
        //   font to be considered for the line height.
        // - Even if it's in the same font, its unnecessary.
        //
        // There is one exception though. When the whole line is empty, we
        // 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() {
                let mut reshaped = shaped.reshape(engine, &p.spans, start..range.end);
                if hyphen || shy {
                    reshaped.push_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.
                        // See Requirements for Chinese Text Layout, Section 3.1.6.3
                        // Compression of punctuation marks at line start or line end
                        let shrink_amount = last_glyph.shrinkability().1;
                        let punct = reshaped.glyphs.to_mut().last_mut().unwrap();
                        punct.shrink_right(shrink_amount);
                        reshaped.width -= shrink_amount.at(reshaped.size);
                    } else if p.cjk_latin_spacing
                        && last_glyph.is_cj_script()
                        && (last_glyph.x_advance - last_glyph.x_offset) > Em::one()
                    {
                        // If the last glyph is a CJK character adjusted by [`add_cjk_latin_spacing`],
                        // restore the original width.
                        let shrink_amount =
                            last_glyph.x_advance - last_glyph.x_offset - Em::one();
                        let glyph = reshaped.glyphs.to_mut().last_mut().unwrap();
                        glyph.x_advance -= shrink_amount;
                        glyph.adjustability.shrinkability.1 = Em::zero();
                        reshaped.width -= shrink_amount.at(reshaped.size);
                    }
                }

                width += reshaped.width;
                last = Some(Item::Text(reshaped));
            }

            inner = before;
        }
    }

    // Deal with CJ characters at line starts.
    let text = &p.bidi.text[range.start..end];
    let maybe_adjust_first_glyph = text.starts_with(BEGIN_PUNCT_PAT)