M3: right-to-left punctuation doesn't decide a line's direction, and an alphabetic mark gives a taken-back neutral no direction (view.copy-text)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-11-08 14:19:30 +08:00
1 parent 8c1977f051
commit aab3682629
2 files changed
+43 -6

No files matched your search

+9 -6
View File
@@ -356,11 +356,13 @@ fn keep_taken_back_blanks(glyphs: &[&TextGlyph], placed: &mut [usize]) {
let mark = |g: &TextGlyph| !g.text.is_empty() && g.text.chars().all(|c| unicode_bidi::bidi_class(c) == unicode_bidi::BidiClass::NSM);
// The direction a glyph gives the text around it: Some(false) for left-to-right letters, Some(true) for
// right-to-left letters and for numbers, which read as right-to-left beside neutrals (UAX #9 N1, as in
// `reverse_rtl`), None for a glyph without one.
// `reverse_rtl`), None for a glyph without one. Nonspacing marks give none, alphabetic ones (U+0345) included: a
// mark has the direction of the letter it sits on.
let strong = |g: &TextGlyph| {
if g.text.chars().any(|c| c.is_alphabetic() && !is_rtl(c)) {
let spacing = || g.text.chars().filter(|&c| unicode_bidi::bidi_class(c) != unicode_bidi::BidiClass::NSM);
if spacing().any(|c| c.is_alphabetic() && !is_rtl(c)) {
Some(false)
} else if g.text.chars().any(char::is_alphanumeric) {
} else if spacing().any(char::is_alphanumeric) {
Some(true)
} else {
None
@@ -597,13 +599,14 @@ fn bidi_class(text: &str) -> Bidi {
}
/// Whether a line of the glyphs `line` is right-to-left: it has more right-to-left letters than left-to-right ones.
/// Letters are counted one by one, so a ligature glyph ("ffi") counts each of its letters, and nonspacing marks count
/// toward neither direction: a mark has the direction of the letter it sits on.
/// Letters are counted one by one, so a ligature glyph ("ffi") counts each of its letters. Punctuation of a
/// right-to-left script (a maqaf, an Arabic comma) is not a letter and counts toward neither direction, nor do
/// nonspacing marks: a mark has the direction of the letter it sits on.
fn is_rtl_line(glyphs: &[&TextGlyph], line: impl Iterator<Item = usize>) -> bool {
let (mut rtl, mut ltr) = (0usize, 0usize);
for g in line.filter_map(|i| glyphs.get(i)) {
for c in g.text.chars().filter(|&c| unicode_bidi::bidi_class(c) != unicode_bidi::BidiClass::NSM) {
if is_rtl(c) && !c.is_numeric() {
if is_rtl(c) && c.is_alphabetic() {
rtl = rtl.saturating_add(1);
} else if c.is_alphabetic() && !is_rtl(c) {
ltr = ltr.saturating_add(1);
+34
View File
@@ -2482,6 +2482,40 @@ fn a_ligature_counts_each_of_its_letters_toward_a_lines_direction() {
assert_eq!(line(&["ffi"]), split);
}
/// Punctuation of a right-to-left script is not a letter and doesn't decide a line's direction: two Latin letters,
/// a Hebrew letter and a maqaf and a sof pasuq after it read Latin first.
#[test]
fn right_to_left_punctuation_does_not_decide_a_lines_direction() {
let mut v = vec![em_box("a", 0.0, 0.0, 6.0, 11.0), em_box("b", 6.0, 0.0, 12.0, 11.0)];
let mut x = 16.0;
for c in ["\u{5C3}", "\u{5BE}", "ש"] {
v.push(em_box(c, x, 0.0, x + 6.5, 11.0));
x += 6.5;
}
let text = layout(v).plain_text();
assert!(text.starts_with("ab"), "{text:?}");
}
/// An alphabetic nonspacing mark (U+0345) gives the text around a taken-back neutral no direction: a question mark
/// taken back over the space that starts a reverse-order show string reads as on the same line without the mark.
#[test]
fn an_alphabetic_mark_beside_a_taken_back_neutral_gives_it_no_direction() {
let line = |mark: bool| {
let mut v = vec![em_box(" ", 10.0, 0.0, 13.5, 11.0), em_box("?", 9.9, 0.0, 15.0, 11.0)];
let mut x = 15.0;
for c in ["ף", "י", "ע", "ס"] {
v.push(em_box(c, x, 0.0, x + 6.5, 11.0));
x += 6.5;
}
if mark {
v.push(em_box("\u{345}", 14.0, 0.0, 14.0, 11.0));
}
layout(v).plain_text()
};
let without = line(false);
assert_eq!(line(true).replace('\u{345}', ""), without);
}
/// A mark of a left-to-right script that rides on no letter has no direction of its own: U+0345 (alphabetic, bidi
/// class NSM) drawn on the last letter of a Hebrew word, left of it and right after a Latin word, reads with the
/// Hebrew word, after that letter, as a Hebrew point drawn there does.