collective

Browse / github.com/storytold/indd-utils / patch

fix: read MatchCharacterStyleType text variables (type code 0xCAAD)

fix: read MatchCharacterStyleType text variables (type code 0xCAAD) ## What A text variable of the "match character style" kind (type code `0xCAAD`, IDML `VariableType="MatchCharacterStyleType"`) is not in the type table, so the converter wrote it as a bare `<TextVariable Self=… Name=…/>`: no `VariableType`, no `MatchCharacterStylePreference`, and its instances had no `ResultText`. This adds the type. The fields after the type code are those of the running header (`MatchParagraphStyleType`): `b` is the style UID (here a character style, written as `AppliedCharacterStyle`), `c` the search strategy, `a` the case change. On a master page the instance shows `Tex

Status
open
Score
0.00
Adopters
0 other operators
Author
jungseohaan (imported from pull request #6, open)
Base
7f3060e5bce78d21ea70e0d5eacb96edce15dfbe
Size
+75 −3 in 3 files
Review
pending
Shared
2026-10-28
Id
5f3f0be185511c22a96cd2bf

How it went

No other operator has reported using it yet.

Diff

raw · docs/format/text-variables.md src/idml/designmap.rs src/model/variable.rs

diff --git a/docs/format/text-variables.md b/docs/format/text-variables.md--- a/docs/format/text-variables.md+++ b/docs/format/text-variables.md@@ -45,6 +45,7 @@ chunk with it (their settings are all reproduced, below). | 0xCAAA | `MatchParagraphStyleType` | 135 | `MatchParagraphStylePreference` | | 0xCAAB | `ModificationDateType` | 135 | `DateVariablePreference` | | 0xCAAC | `CreationDateType` | 135 | `DateVariablePreference` |+| 0xCAAD | `MatchCharacterStyleType` | 282 (privately held pairs, below) | `MatchCharacterStylePreference` | | 0xCAB5 | `XrefPageNumberType` | 135 | none | | 0xCAB6 | `XrefChapterNumberType` | 135 | none | | 0xCAC0 | `LiveCaptionType` | 130 | `CaptionMetadataVariablePreference` |@@ -88,6 +89,20 @@ settings is a paragraph style UID. It differs between files, but in all   0 or 1 except in 2 definitions, in files without IDML, where it is   0x40. That may be a flag bit for this attribute; which one is open, so   those definitions get no `DeleteEndPunctuation`.+- Character style match (0xCAAD): the fields are those of the running+  header. *b* is the character style UID, written as+  `AppliedCharacterStyle`; *c* is `SearchStrategy`, 0 `FirstOnPage`,+  1 `LastOnPage`; *a* is `ChangeCase`, 0 or 0xCAC9 `None`. The+  `MatchCharacterStylePreference` element has the same attributes as+  `MatchParagraphStylePreference` with `AppliedCharacterStyle` in place of+  `AppliedParagraphStyle`. The type table above was drawn from the public+  corpus and lacks this code. Over 282 pairs held privately (826+  definitions), the converter's `TextVariable` element, including+  its settings element, equals the reference IDML's in 282 of 282 pairs.+  The values seen are `FirstOnPage`, `None`, `DeleteEndPunctuation="false"`+  and an empty `TextBefore` and `TextAfter`; a variable with another+  value in *a* or *c* has not been seen, so those mappings are those of+  the running header and not independently proven. - Chapter number (0xCAA9): *a* is `Format`, 0 `Current`; 0x4C15   `Arabic` (one definition, in a trustworthy pair). @@ -156,6 +171,13 @@ In the corpus of 2026-10 two more kinds of instance show their text:  - A running header on a master page shows `TextBefore`, `<`, the   variable's name, `>` and `TextAfter`.+- A character style match on a master page shows the same as a running+  header there: `TextBefore`, `<`, the variable's name, `>` and+  `TextAfter`. On a page, the instance shows the text found, or the+  same placeholder when nothing matches; that depends on the layout, so+  no `ResultText` is written for it. Over the 282 pairs above the+  converter writes `ResultText` for 3,026 instances, all equal to the+  reference, and leaves out 883 on pages, where the reference has one. - A chapter number in the `Current` format shows `TextBefore`, the   document's chapter number (`ChapterNumberPreference`) and   `TextAfter`.diff --git a/src/idml/designmap.rs b/src/idml/designmap.rs--- a/src/idml/designmap.rs+++ b/src/idml/designmap.rs@@ -1322,6 +1322,38 @@ impl Writer<'_> {                 }                 x.end();             }+            0xCAAD => {+                x.start("MatchCharacterStylePreference");+                text(x, "TextBefore");+                text(x, "TextAfter");+                x.attr("AppliedCharacterStyle", self.style_ref(v.style, false));+                if let Some(st) = &v.settings {+                    match st.c {+                        0 => {+                            x.attr("SearchStrategy", "FirstOnPage");+                        }+                        1 => {+                            x.attr("SearchStrategy", "LastOnPage");+                        }+                        _ => {}+                    }+                    match st.a {+                        0 | 0xCAC9 => {+                            x.attr("ChangeCase", "None");+                        }+                        0xCAD0 => {+                            x.attr("ChangeCase", "Sentencecase");+                        }+                        _ => {}+                    }+                }+                // Every corpus sample is `false`; no field is known for it+                // (text-variables.md, running headers).+                if same || v.settings.as_ref().is_some_and(|st| st.c <= 1) {+                    x.attr("DeleteEndPunctuation", "false");+                }+                x.end();+            }             // Chapter numbers: the format code is the first u32             // (text-variables.md, settings).             0xCAA9 if v.settings.is_some() => {@@ -1421,7 +1453,7 @@ impl Writer<'_> {                     .unwrap_or(&self.name)                     .to_string(),             ),-            Some(d) if d.kind == 0xCAAA && self.on_master(story) => {+            Some(d) if matches!(d.kind, 0xCAAA | 0xCAAD) && self.on_master(story) => {                 around(d, &format!("<{}>", d.name))             }             Some(d) if d.kind == 0xCAA9 && d.settings.as_ref().is_some_and(|s| s.a == 0) => selfdiff --git a/src/model/variable.rs b/src/model/variable.rs--- a/src/model/variable.rs+++ b/src/model/variable.rs@@ -20,6 +20,7 @@ pub const TYPES: &[(u32, &str)] = &[     (0xCAA9, "ChapterNumberType"),     (0xCAAA, "MatchParagraphStyleType"),     (0xCAAB, "ModificationDateType"),+    (0xCAAD, "MatchCharacterStyleType"),     (0xCAAC, "CreationDateType"),     (0xCAB5, "XrefPageNumberType"),     (0xCAB6, "XrefChapterNumberType"),@@ -35,7 +36,8 @@ pub struct TextVariable {     /// The date format of date variables; the contents of custom text     /// variables (not proven, see the format notes).     pub text: String,-    /// Paragraph style of a running header (`MatchParagraphStyleType`).+    /// Paragraph style of a running header (`MatchParagraphStyleType`), or+    /// the character style a `MatchCharacterStyleType` variable follows.     pub style: Option<u32>,     /// The fields not identified hold the values every corpus sample of     /// this type has, so the IDML settings are those of the samples.@@ -112,7 +114,7 @@ fn parse(enc: Encoding, uid: u32, data: &[u8]) -> Result<TextVariable, Error> {                 (zero(&rest[..4]) || enc.u32_at(rest, 0) == Some(0xCAB3)) && zero(&rest[4..])             }             0xCAC0 => enc.u32_at(rest, 0) == Some(0x8C64) && zero(&rest[4..]),-            0xCAAA => {+            0xCAAA | 0xCAAD => {                 style = enc.u32_at(rest, 4);                  zero(&rest[..4]) && zero(&rest[8..])@@ -200,4 +202,20 @@ mod tests {                 .as_in_samples         );     }++    #[test]+    fn reads_the_character_style_of_a_match_variable() {+        let mut d = counted_text("Header");+        d.extend(counted_text(""));+        d.extend(0xCAADu32.to_le_bytes());+        d.extend(0xCAC9u32.to_le_bytes());+        d.extend(0x81u32.to_le_bytes());+        // *c*, then the empty texts before and after.+        d.extend([0; 12]);+        let v = parse(crate::object::Encoding::default(), 0x8A, &d).unwrap();+        assert_eq!(v.type_name(), Some("MatchCharacterStyleType"));+        assert_eq!(v.style, Some(0x81));+        let s = v.settings.unwrap();+        assert_eq!((s.a, s.b, s.c), (0xCAC9, 0x81, 0));+    } }