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.rs — test_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.
Summary
A mandatory line break (
U+000A, UAX #14 classBK) 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 published0.11.1, with and without thecomplex-scriptsfeature.Reproduction
No font is named, so this should be portable.
Expected
Every row
2/2.U+000Ais classBK; the break is mandatory and unconditional.Actual
Line::break_reason()on the merged line returnsBreakReason::None, notExplicit— the boundary appears to be lost rather than deprioritised.What I ruled out
break_all_lines(None),Some(f32::MAX),Some(10000.0)andSome(984.0)all behave identically.complex-scriptsfeature. 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.)0x0A, noU+2028.icu_segmenter. Queried directly at the same versioncargo tree -ishows parley linking (v2.3.0, a single shared node), it reports the mandatory boundary correctly in every case. For"ฉันกำลังถือแมงมุม\nตัวใหญ่ไว้ในมือ"the boundary set contains both51(dictionary boundary) and52(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\nalso 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 withBreakReason::Explicit, split at the authored\n.WordBreak::BreakAllalso works.Possibly why it survived
The mandatory-break tests in
src/tests/test_analysis.rs—test_mandatory_break_in_text("ABC DEF\nG"),test_newline,test_two_newlines— are Latin-only. Nothing appears to exercise\ninside a complex-script run, so a regression here would not be caught.Environment
main@11711d8f67d3c033d07ff7b5db55021da183d1a2, and0.11.1from crates.ioicu_segmenter2.3.0,compiled_dataHappy to test a patch or add the cases to the test suite if that would help.