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
@@ -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`.@@ -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) => self@@ -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));+ } }