Skip to content

Mandatory line break (U+000A) silently dropped inside Thai, Khmer and Lao runs #768

Description

@MBehtemam

Summary

A mandatory line break (U+000A, UAX #14 class BK) is silently dropped when it sits inside a run of Thai, Khmer or Lao. The layout returns one line where two were asked for, with no error and no warning.

Reproduces on main (11711d8) and on the published 0.11.1, with and without the complex-scripts feature.

Reproduction

No font is named, so this should be portable.

use parley::{FontContext, LayoutContext, StyleProperty};

fn lines(text: &str) -> usize {
    let mut fcx = FontContext::new();
    let mut lcx: LayoutContext<[u8; 4]> = LayoutContext::new();
    let mut b = lcx.ranged_builder(&mut fcx, text, 1.0, false);
    b.push_default(StyleProperty::FontSize(32.0));
    let mut l = b.build(text);
    l.break_all_lines(None);
    l.lines().count()
}

fn main() {
    for (text, label) in [
        ("AB\nC",      "latin"),
        ("กก\nกก",      "thai"),
        ("ក្ម\nក្ម",      "khmer"),
        ("ກກ\nກກ",      "lao"),
        ("日本\n語",    "japanese"),
        ("กกA\nB",      "thai, latin before the LF"),
        ("กก \nกก",     "thai, space before the LF"),
    ] {
        let (got, want) = (lines(text), text.matches('\n').count() + 1);
        println!("{:<28} {got}/{want}  {}", label, if got == want { "ok" } else { "WRONG" });
    }
}

Expected

Every row 2/2. U+000A is class BK; the break is mandatory and unconditional.

Actual

latin                        2/2  ok
thai                         1/2  WRONG
khmer                        1/2  WRONG
lao                          1/2  WRONG
japanese                     2/2  ok
thai, latin before the LF    2/2  ok
thai, space before the LF    2/2  ok

Line::break_reason() on the merged line returns BreakReason::None, not Explicit — the boundary appears to be lost rather than deprioritised.

What I ruled out

  • Not width-dependent. break_all_lines(None), Some(f32::MAX), Some(10000.0) and Some(984.0) all behave identically.
  • Not font-dependent. Reproduces with no font named, and with Ayuthaya, Thonburi, Helvetica and Arial Unicode MS.
  • Not the complex-scripts feature. Identical with it on and off. (Feature activation was verified behaviourally, not by inspecting flags: an unspaced Thai paragraph in a 420 px box gives 1 line with the feature off and 5 with it on.)
  • Not the input bytes. Byte-dumped: a literal 0x0A, no U+2028.
  • Not icu_segmenter. Queried directly at the same version cargo tree -i shows parley linking (v2.3.0, a single shared node), it reports the mandatory boundary correctly in every case. For "ฉันกำลังถือแมงมุม\nตัวใหญ่ไว้ในมือ" the boundary set contains both 51 (dictionary boundary) and 52 (mandatory, after the LF).

Trigger

Not simply "the character before the LF" — "กกA\nB" and "Aก\nB" both pass. Thai adjacent to the LF appears necessary but not sufficient; a single Thai character does not trigger it, a longer run does. A space or a Latin character immediately before the LF prevents it. \r\n also survives, presumably because CR takes a separate rule path.

I have not diagnosed it further than reproducing from the public API. It looks like the sort of thing #676 describes — a complex-script sub-range's opportunities being spliced back over the global list and overwriting the boundary at the range's end — but that is a guess, not a diagnosis.

Workaround

StyleProperty::TextWrapMode(TextWrapMode::NoWrap) produces the correct 2 lines with BreakReason::Explicit, split at the authored \n. WordBreak::BreakAll also works.

Possibly why it survived

The mandatory-break tests in src/tests/test_analysis.rstest_mandatory_break_in_text ("ABC DEF\nG"), test_newline, test_two_newlines — are Latin-only. Nothing appears to exercise \n inside a complex-script run, so a regression here would not be caught.

Environment

  • parley main @ 11711d8f67d3c033d07ff7b5db55021da183d1a2, and 0.11.1 from crates.io
  • icu_segmenter 2.3.0, compiled_data
  • rustc 1.95.0, macOS 15 (arm64). Not tested on another platform — but the failing path is text analysis with no OS involvement, and the segmentation data is baked in, so I would expect it to be platform-independent.

Happy to test a patch or add the cases to the test suite if that would help.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions