mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M3: a mark takes the class of the letter it rides on, and the paragraph direction counts letters before marks (view.copy-text)
- resolve_marks applies UAX #9 W1 only to a mark riding on the glyph before it (its anchor), so a Hebrew mark placed after a Latin letter keeps its own class and stays with its word. - reverse_rtl takes the paragraph direction before marks take their letter's class, as keep_taken_back_blanks counts the line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
This commit is contained in:
1 parent
c4cc9e0c37
commit
ee4b044293
3 files changed
+31
-9
No files matched your search
@@ -509,7 +509,7 @@ fn arrange(glyphs: &[TextGlyph]) -> (Vec<usize>, Vec<u32>, Vec<bool>) {
|
||||
if has_rtl {
|
||||
sc.logical.clear();
|
||||
sc.logical.extend_from_slice(&sc.placed);
|
||||
reverse_rtl(&glyphs, &mut sc.logical, &mut spaces);
|
||||
reverse_rtl(&glyphs, &sc.anchor, &mut sc.logical, &mut spaces);
|
||||
}
|
||||
let idx: &[usize] = if has_rtl { &sc.logical } else { &sc.placed };
|
||||
order.extend_from_slice(idx);
|
||||
|
||||
@@ -593,19 +593,23 @@ fn bidi_class(text: &str) -> Bidi {
|
||||
}
|
||||
}
|
||||
|
||||
/// UAX #9 W1: a nonspacing mark takes the class of the glyph before it, the letter it rides on, which placement keeps
|
||||
/// right in front of it. A mark after a word gap (`None`) rides on nothing and keeps its own class.
|
||||
fn resolve_marks(glyphs: &[&TextGlyph], items: &mut [(Option<usize>, Bidi)]) {
|
||||
/// UAX #9 W1: a nonspacing mark takes the class of the letter it rides on ([`Scratch::anchor`]), which placement keeps
|
||||
/// right in front of it. A mark that rides on nothing (one after a word gap, or a right-to-left mark, which placement
|
||||
/// puts by its own position) keeps its own class.
|
||||
fn resolve_marks(glyphs: &[&TextGlyph], anchor: &[usize], items: &mut [(Option<usize>, Bidi)]) {
|
||||
let nsm = |i: usize| {
|
||||
glyphs.get(i).is_some_and(|g| !g.text.is_empty() && g.text.chars().all(|c| unicode_bidi::bidi_class(c) == unicode_bidi::BidiClass::NSM))
|
||||
};
|
||||
for k in 1..items.len() {
|
||||
let base = match items.get(k - 1) {
|
||||
Some(&(Some(_), base)) => base,
|
||||
let (prev, base) = match items.get(k - 1) {
|
||||
Some(&(Some(prev), base)) => (prev, base),
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(it) = items.get_mut(k)
|
||||
&& it.0.is_some_and(nsm)
|
||||
&& let Some(mark) = it.0
|
||||
&& nsm(mark)
|
||||
&& anchor_of(anchor, mark) != mark
|
||||
&& anchor_of(anchor, mark) == anchor_of(anchor, prev)
|
||||
{
|
||||
it.1 = base;
|
||||
}
|
||||
@@ -616,7 +620,7 @@ fn resolve_marks(glyphs: &[&TextGlyph], items: &mut [(Option<usize>, Bidi)]) {
|
||||
/// word gaps along: `spaces[w]` is a gap left of `idx[w]` (#1062). The paragraph is right-to-left when it has
|
||||
/// more right-to-left than left-to-right glyphs; numbers keep their left-to-right order, and neutrals
|
||||
/// (punctuation, gaps) take the direction of their neighbours, or the paragraph's between opposite ones.
|
||||
pub(super) fn reverse_rtl(glyphs: &[&TextGlyph], idx: &mut [usize], spaces: &mut [bool]) {
|
||||
pub(super) fn reverse_rtl(glyphs: &[&TextGlyph], anchor: &[usize], idx: &mut [usize], spaces: &mut [bool]) {
|
||||
// Glyphs and word gaps (None) in visual order.
|
||||
let mut items: Vec<(Option<usize>, Bidi)> = Vec::with_capacity(idx.len() * 2);
|
||||
for (w, &i) in idx.iter().enumerate() {
|
||||
@@ -627,9 +631,11 @@ pub(super) fn reverse_rtl(glyphs: &[&TextGlyph], idx: &mut [usize], spaces: &mut
|
||||
}
|
||||
let texts: Vec<&str> = items.iter().map(|it| it.0.map_or("", |i| glyphs[i].text.as_str())).collect();
|
||||
let text = |k: usize| texts.get(k).copied().unwrap_or("");
|
||||
resolve_marks(glyphs, &mut items);
|
||||
// The paragraph's direction counts letters as they are, before marks take their letter's class, as
|
||||
// `keep_taken_back_blanks` counts them.
|
||||
let count = |b: Bidi| items.iter().filter(|it| it.1 == b).count();
|
||||
let para = if count(Bidi::R) > count(Bidi::L) { Bidi::R } else { Bidi::L };
|
||||
resolve_marks(glyphs, anchor, &mut items);
|
||||
let n = items.len();
|
||||
// W4: a single separator between two digits belongs to the number.
|
||||
for k in 1..n.saturating_sub(1) {
|
||||
|
||||
@@ -2416,3 +2416,19 @@ fn a_mark_reaching_left_of_its_letter_does_not_reorder_overprinting_runs() {
|
||||
assert!(text.starts_with("ABCDEF"), "{text:?}");
|
||||
assert!(text.contains("c\u{301}d"), "{text:?}");
|
||||
}
|
||||
|
||||
/// A right-to-left mark rides on no glyph, so it keeps its own class: one placed after a Latin
|
||||
/// letter, left of the Hebrew letter it belongs to, still reads with the Hebrew word, after its letter.
|
||||
#[test]
|
||||
fn a_hebrew_mark_after_a_latin_letter_stays_with_the_hebrew_word() {
|
||||
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)];
|
||||
v.push(em_box("\u{5B8}", 11.9, 0.0, 11.9, 11.0));
|
||||
let mut x = 12.0;
|
||||
for c in ["ם", "ו", "ל", "ש"] {
|
||||
v.push(em_box(c, x, 0.0, x + 6.5, 11.0));
|
||||
x += 6.5;
|
||||
}
|
||||
let text = layout(v).plain_text();
|
||||
// The mark follows its letter, the last of "שלום", before the Latin text.
|
||||
assert_eq!(text, "שלום\u{5B8}ab");
|
||||
}
|
||||
Reference in new issue
Block a user