SVG import: direction keywords match in any letter case

SVG import compared `direction` with `rtl` case-sensitively, so
`direction="RTL"` or `style="direction: Rtl"` read as left to right.
CSS keywords are ASCII case-insensitive
(https://www.w3.org/TR/css-values-3/#keywords), and `unicode-bidi` was
already read that way.

The `<tspan>` embedding direction, the `<text>` paragraph direction and
the anchoring of each chunk now read `direction` through one helper
that compares without regard to case.
This commit is contained in:
dotCooCoo
2026-11-14 03:47:38 -08:00
parent edb228ac33
commit 78dd7dd765
2 changed files with 15 additions and 4 deletions
+9 -3
View File
@@ -965,6 +965,12 @@ impl Ctx {
"normal".into()
}
/// Whether the computed `direction` of element `n` is `rtl`, in any letter case (CSS keywords
/// are ASCII case-insensitive, https://www.w3.org/TR/css-values-3/#keywords).
fn is_rtl(&self, n: XNode) -> bool {
self.prop(n, "direction").is_some_and(|d| d.trim().eq_ignore_ascii_case("rtl"))
}
/// The Character Direction of the characters of element `n` inside a `<text>`, and what it
/// leaves out (SVG 1.1 §10.7.4,
/// https://www.w3.org/TR/SVG11/text.html#RelationshipWithBi-directionality, and CSS Writing
@@ -987,7 +993,7 @@ impl Ctx {
let inherited = parent.as_ref().and_then(|p| p.style.char_direction);
let mut notes = parent.map_or_else(Vec::new, |p| p.bidi_notes.clone());
let rtl = |rtl: bool| Some(if rtl { ParaDirection::RightToLeft } else { ParaDirection::LeftToRight });
let own = || rtl(self.prop(n, "direction").is_some_and(|d| d.trim() == "rtl"));
let own = || rtl(self.is_rtl(n));
let (dir, note) = match bidi.as_str() {
"embed" => (own(), None),
"bidi-override" => (own(), Some(BIDI_OVERRIDE)),
@@ -1359,7 +1365,7 @@ impl Ctx {
// With `unicode-bidi: plaintext`, the text runs in the direction of its first strong
// character, and each paragraph in that of its own ([`pin_first_strong`]).
let plaintext = self.unicode_bidi(t) == "plaintext";
let rtl = if plaintext { self.first_strong_rtl(t) == Some(true) } else { self.prop(t, "direction").is_some_and(|d| d.trim() == "rtl") };
let rtl = if plaintext { self.first_strong_rtl(t) == Some(true) } else { self.is_rtl(t) };
let pin = |obj: &mut TextObject| {
pin_direction(obj, rtl);
if plaintext {
@@ -1386,7 +1392,7 @@ impl Ctx {
let chunk: String = cs.chars[i..end].iter().map(|c| c.0).collect();
text_first_strong_rtl(&chunk) == Some(true)
};
let rtl = if plaintext { chunk_rtl() } else { self.prop(el, "direction").is_some_and(|d| d.trim() == "rtl") };
let rtl = if plaintext { chunk_rtl() } else { self.is_rtl(el) };
if !vertical && rtl { 1.0 - a } else { a }
};
let anchor = anchor_of(0);
+6 -1
View File
@@ -391,6 +391,9 @@ fn rtl_point_text_start_and_end_use_directional_edges() {
(r#"<text x="200" y="60" direction="rtl" font-size="20"><tspan direction="ltr">Wave</tspan></text>"#, 0.0),
(r#"<text x="200" y="60" text-anchor="start" font-size="20">Wave</text>"#, 0.0),
(r#"<text x="200" y="60" direction="ltr" text-anchor="end" font-size="20">Wave</text>"#, 1.0),
// CSS keywords are case-insensitive.
(r#"<text x="200" y="60" direction="RTL" font-size="20">Wave</text>"#, 1.0),
(r#"<text x="200" y="60" style="direction: Rtl" text-anchor="end" font-size="20">Wave</text>"#, 0.0),
// With `unicode-bidi: plaintext`, the direction of the chunk's first strong character.
(r#"<text x="200" y="60" unicode-bidi="plaintext" font-size="20">&#x5D0;&#x5D1;&#x5D2;</text>"#, 1.0),
(r#"<text x="200" y="60" unicode-bidi="plaintext" text-anchor="end" font-size="20">12 &#x5D0;&#x5D1;</text>"#, 0.0),
@@ -494,6 +497,8 @@ fn a_tspan_bidi_embedding_becomes_its_character_direction() {
("ltr", "rtl", "normal", None, false),
("rtl", "ltr", "normal", None, false),
("ltr", "rtl", "EMBED", Some(Rtl), false),
("ltr", "RTL", "embed", Some(Rtl), false),
("RTL", "ltr", "embed", Some(Ltr), false),
("ltr", "rtl", "sideways", None, false),
] {
let (t, w) = read(&format!(
@@ -503,7 +508,7 @@ fn a_tspan_bidi_embedding_becomes_its_character_direction() {
assert_eq!(t.plain_text(), "ab 1 2 cd", "{case}: the characters stay in logical order");
assert_eq!(char_direction_of(&t, "1 2"), want, "{case}: {:?}", t.runs);
assert_eq!(char_direction_of(&t, "ab"), None, "{case}");
assert_eq!(t.para_at(0).direction == Some(Rtl), outer == "rtl", "{case}: the paragraph keeps the text's direction");
assert_eq!(t.para_at(0).direction == Some(Rtl), outer.eq_ignore_ascii_case("rtl"), "{case}: the paragraph keeps the text's direction");
assert_eq!(w.iter().any(|w| w.contains("bidi-override") && w.contains("not supported")), warns, "{case}: {w:?}");
if !warns {
assert!(w.is_empty(), "{case}: {w:?}");