Skip to main content

ttml_subtitle/
document.rs

1//! TTML document structure — W3C TTML2 §3 (element syntax).
2//!
3//! This module defines the full element tree for a TTML2 document. Each element
4//! type captures all attributes listed in its syntax box (see `ttml2-syntax.md` §3)
5//! plus any namespace-qualified attributes in the TT Style Namespaces, TT Metadata
6//! Namespace, and TT Parameter Namespace.
7//!
8//! Parsing is done via `roxmltree`; the parsed document tree is a fully typed
9//! Rust structure that does NOT contain the original XML text (no raw-passthrough).
10
11extern crate alloc;
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::String;
15use alloc::string::ToString;
16use alloc::vec::Vec;
17
18use alloc::collections::BTreeMap;
19
20use crate::error::{Error, Result};
21
22// ─── Namespace constants ───────────────────────────────────────────
23
24/// TTML namespace: `http://www.w3.org/ns/ttml`
25pub const NS_TT: &str = "http://www.w3.org/ns/ttml";
26/// TT Parameter namespace: `http://www.w3.org/ns/ttml#parameter`
27pub const NS_TTP: &str = "http://www.w3.org/ns/ttml#parameter";
28/// TT Style namespace: `http://www.w3.org/ns/ttml#styling`
29pub const NS_TTS: &str = "http://www.w3.org/ns/ttml#styling";
30/// TT Audio Style namespace: `http://www.w3.org/ns/ttml#audio`
31pub const NS_TTA: &str = "http://www.w3.org/ns/ttml#audio";
32/// TT Metadata namespace: `http://www.w3.org/ns/ttml#metadata`
33pub const NS_TTM: &str = "http://www.w3.org/ns/ttml#metadata";
34/// TT Profile namespace: `http://www.w3.org/ns/ttml/profile/`
35pub const NS_TT_PROFILE: &str = "http://www.w3.org/ns/ttml/profile/";
36/// TT Feature namespace: `http://www.w3.org/ns/ttml/feature/`
37pub const NS_TT_FEATURE: &str = "http://www.w3.org/ns/ttml/feature/";
38/// IMSC Styling namespace: `http://www.w3.org/ns/ttml/profile/imsc1#styling`
39pub const NS_ITTS: &str = "http://www.w3.org/ns/ttml/profile/imsc1#styling";
40/// IMSC Parameter namespace: `http://www.w3.org/ns/ttml/profile/imsc1#parameter`
41pub const NS_ITTP: &str = "http://www.w3.org/ns/ttml/profile/imsc1#parameter";
42/// IMSC Metadata namespace: `http://www.w3.org/ns/ttml/profile/imsc1#metadata`
43pub const NS_ITTM: &str = "http://www.w3.org/ns/ttml/profile/imsc1#metadata";
44/// EBU-TT Styling namespace: `urn:ebu:tt:style`
45pub const NS_EBUTTS: &str = "urn:ebu:tt:style";
46/// EBU-TT Metadata namespace: `urn:ebu:tt:metadata`
47pub const NS_EBUTTM: &str = "urn:ebu:tt:metadata";
48/// SMPTE-TT Extension namespace: `http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt`
49pub const NS_SMPTE: &str = "http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt";
50/// XML namespace: `http://www.w3.org/XML/1998/namespace`
51pub const NS_XML: &str = "http://www.w3.org/XML/1998/namespace";
52
53// ─── IMSC Profile Designators ──────────────────────────────────────
54
55/// IMSC 1.1 Text Profile designator — IMSC 1.1 §8.1.
56pub const IMSC11_TEXT_PROFILE: &str = "http://www.w3.org/ns/ttml/profile/imsc1.1/text";
57/// IMSC 1.1 Image Profile designator — IMSC 1.1 §9.1.
58pub const IMSC11_IMAGE_PROFILE: &str = "http://www.w3.org/ns/ttml/profile/imsc1.1/image";
59/// IMSC 1.0/1.0.1 Text Profile designator.
60pub const IMSC1_TEXT_PROFILE: &str = "http://www.w3.org/ns/ttml/profile/imsc1/text";
61/// IMSC 1.0/1.0.1 Image Profile designator.
62pub const IMSC1_IMAGE_PROFILE: &str = "http://www.w3.org/ns/ttml/profile/imsc1/image";
63
64// ─── Document root ─────────────────────────────────────────────────
65
66/// A parsed TTML document.
67///
68/// This is the top-level type. Create one with [`Document::parse_str`].
69#[derive(Debug, Clone, PartialEq)]
70#[non_exhaustive]
71pub struct Document {
72    /// The root `<tt>` element.
73    pub tt: TtElement,
74    /// Any XML declaration attributes (version, encoding).
75    pub xml_declaration: Option<XmlDeclaration>,
76}
77
78/// Parsed XML declaration.
79#[derive(Debug, Clone, PartialEq)]
80#[non_exhaustive]
81pub struct XmlDeclaration {
82    /// XML version, e.g. "1.0".
83    pub version: String,
84    /// XML encoding, e.g. "UTF-8".
85    pub encoding: String,
86}
87
88impl Document {
89    /// Create a new empty document for from-scratch construction.
90    ///
91    /// Use this to build TTML documents programmatically.
92    /// Fields marked `#[non_exhaustive]` can be constructed using
93    /// `..Default::default()` where `Default` is implemented.
94    pub fn new() -> Self {
95        Document {
96            tt: TtElement::default(),
97            xml_declaration: None,
98        }
99    }
100
101    /// Parse a TTML document from an XML string.
102    pub fn parse_str(xml: &str) -> Result<Self> {
103        let doc = roxmltree::Document::parse(xml).map_err(|e| Error::XmlParse(e.to_string()))?;
104
105        let root = doc.root_element();
106
107        // The root element might be nested if there's an XML declaration;
108        // find the <tt> element.
109        let tt_node = root
110            .children()
111            .find(|n| n.is_element() && n.tag_name().name() == "tt")
112            .or_else(|| {
113                if root.tag_name().name() == "tt" {
114                    Some(root)
115                } else {
116                    None
117                }
118            })
119            .ok_or_else(|| Error::NotTtmlRoot(root.tag_name().name().to_string()))?;
120
121        let tt_ns = tt_node.tag_name().namespace();
122        if tt_ns != Some(NS_TT) {
123            return Err(Error::NotTtmlRoot(format!(
124                "namespace {:?}",
125                tt_ns.unwrap_or("(none)")
126            )));
127        }
128
129        let tt = parse_tt_element(tt_node)?;
130
131        Ok(Document {
132            tt,
133            xml_declaration: None,
134        })
135    }
136
137    /// Serialize this document to an XML string.
138    pub fn to_xml(&self) -> String {
139        let mut buf = String::new();
140        buf.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
141        buf.push('\n');
142        serialize_tt_element(&self.tt, &mut buf, 0);
143        // Ensure trailing newline
144        if !buf.ends_with('\n') {
145            buf.push('\n');
146        }
147        buf
148    }
149
150    /// Get the effective time context from the root `<tt>` element's parameter attributes.
151    pub fn time_context(&self) -> crate::time::TimeContext {
152        self.tt.time_context()
153    }
154}
155
156impl Default for Document {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162// ─── Element types ─────────────────────────────────────────────────
163
164/// The root `<tt>` element — TTML2 §8.1.1.
165#[derive(Debug, Clone, PartialEq, Default)]
166#[non_exhaustive]
167pub struct TtElement {
168    /// XML language, e.g. "en".
169    pub xml_lang: Option<String>,
170    /// XML id.
171    pub xml_id: Option<String>,
172    /// XML space: "default" or "preserve".
173    pub xml_space: Option<XmlSpace>,
174    /// `ttp:timeBase` — default "media".
175    pub ttp_time_base: Option<String>,
176    /// `ttp:frameRate` — default 30.
177    pub ttp_frame_rate: Option<String>,
178    /// `ttp:frameRateMultiplier` — default "1 1".
179    pub ttp_frame_rate_multiplier: Option<String>,
180    /// `ttp:tickRate` — default derived.
181    pub ttp_tick_rate: Option<String>,
182    /// `ttp:subFrameRate` — default 1.
183    pub ttp_sub_frame_rate: Option<String>,
184    /// `ttp:dropMode` — default "nonDrop".
185    pub ttp_drop_mode: Option<String>,
186    /// `ttp:markerMode` — default "discontinuous".
187    pub ttp_marker_mode: Option<String>,
188    /// `ttp:clockMode` — default "utc".
189    pub ttp_clock_mode: Option<String>,
190    /// `ttp:cellResolution` — default "32 15".
191    pub ttp_cell_resolution: Option<String>,
192    /// `ttp:pixelAspectRatio` — no default.
193    pub ttp_pixel_aspect_ratio: Option<String>,
194    /// `ttp:displayAspectRatio` — no default.
195    pub ttp_display_aspect_ratio: Option<String>,
196    /// `ttp:profile` attribute (the simple profile designator).
197    pub ttp_profile: Option<String>,
198    /// `ttp:contentProfiles` — space-separated designators or `all(...)`.
199    pub ttp_content_profiles: Option<String>,
200    /// `ttp:contentProfileCombination`.
201    pub ttp_content_profile_combination: Option<String>,
202    /// `ttp:processorProfiles`.
203    pub ttp_processor_profiles: Option<String>,
204    /// `ttp:processorProfileCombination`.
205    pub ttp_processor_profile_combination: Option<String>,
206    /// `ttp:inferProcessorProfileMethod`.
207    pub ttp_infer_processor_profile_method: Option<String>,
208    /// `ttp:inferProcessorProfileSource`.
209    pub ttp_infer_processor_profile_source: Option<String>,
210    /// `ttp:permitFeatureNarrowing`.
211    pub ttp_permit_feature_narrowing: Option<String>,
212    /// `ttp:permitFeatureWidening`.
213    pub ttp_permit_feature_widening: Option<String>,
214    /// `ttp:validation`.
215    pub ttp_validation: Option<String>,
216    /// `ttp:validationAction`.
217    pub ttp_validation_action: Option<String>,
218    /// `tts:extent` on the root element.
219    pub tts_extent: Option<String>,
220    /// `ittp:activeArea` — IMSC extension (IMSC 1.1 §7.8.5).
221    pub ittp_active_area: Option<String>,
222    /// `ittp:aspectRatio` — IMSC extension (deprecated, IMSC 1.1 §7.8.1).
223    pub ittp_aspect_ratio: Option<String>,
224    /// `ittp:progressivelyDecodable` — IMSC extension (IMSC 1.1 §7.8.2).
225    pub ittp_progressively_decodable: Option<String>,
226    /// Other attributes not covered explicitly (reserved for future extensions).
227    pub other_attributes: BTreeMap<(String, String), String>,
228    /// Optional `<head>` child.
229    pub head: Option<HeadElement>,
230    /// Optional `<body>` child.
231    pub body: Option<BodyElement>,
232    /// Text content (if any) — should be empty per spec.
233    pub text: Option<String>,
234}
235
236impl TtElement {
237    /// Derive the time context from this element's parameter attributes.
238    pub fn time_context(&self) -> crate::time::TimeContext {
239        use crate::time::{ClockMode, DropMode, MarkerMode, TimeBase};
240
241        let time_base = match self.ttp_time_base.as_deref() {
242            Some("smpte") => TimeBase::Smpte,
243            Some("clock") => TimeBase::Clock,
244            _ => TimeBase::Media,
245        };
246
247        let frame_rate: u32 = self
248            .ttp_frame_rate
249            .as_deref()
250            .and_then(|s| s.parse().ok())
251            .unwrap_or(30);
252
253        let (frame_mult_num, frame_mult_den) = if let Some(s) = &self.ttp_frame_rate_multiplier {
254            let parts: Vec<&str> = s.split_whitespace().collect();
255            if parts.len() == 2 {
256                (parts[0].parse().unwrap_or(1), parts[1].parse().unwrap_or(1))
257            } else {
258                (1, 1)
259            }
260        } else {
261            (1, 1)
262        };
263
264        let sub_frame_rate: u32 = self
265            .ttp_sub_frame_rate
266            .as_deref()
267            .and_then(|s| s.parse().ok())
268            .unwrap_or(1);
269
270        let tick_rate: u32 = self
271            .ttp_tick_rate
272            .as_deref()
273            .and_then(|s| s.parse().ok())
274            .unwrap_or(frame_rate * sub_frame_rate);
275
276        let drop_mode = match self.ttp_drop_mode.as_deref() {
277            Some("dropNTSC") => DropMode::DropNtsc,
278            Some("dropPAL") => DropMode::DropPal,
279            _ => DropMode::NonDrop,
280        };
281
282        let marker_mode = match self.ttp_marker_mode.as_deref() {
283            Some("continuous") => MarkerMode::Continuous,
284            _ => MarkerMode::Discontinuous,
285        };
286
287        let clock_mode = match self.ttp_clock_mode.as_deref() {
288            Some("local") => ClockMode::Local,
289            Some("gps") => ClockMode::Gps,
290            _ => ClockMode::Utc,
291        };
292
293        crate::time::TimeContext {
294            time_base,
295            frame_rate,
296            frame_rate_multiplier_numerator: frame_mult_num,
297            frame_rate_multiplier_denominator: frame_mult_den,
298            sub_frame_rate,
299            tick_rate,
300            drop_mode,
301            marker_mode,
302            clock_mode,
303        }
304    }
305}
306
307/// `<head>` element — TTML2 §8.1.2.
308#[derive(Debug, Clone, PartialEq, Default)]
309#[non_exhaustive]
310pub struct HeadElement {
311    /// XML id.
312    pub xml_id: Option<String>,
313    /// XML language.
314    pub xml_lang: Option<String>,
315    /// XML space.
316    pub xml_space: Option<XmlSpace>,
317    /// Metadata children.
318    pub metadata: Vec<MetadataChild>,
319    /// Styling section.
320    pub styling: Option<StylingElement>,
321    /// Layout section.
322    pub layout: Option<LayoutElement>,
323}
324
325/// `<body>` element — TTML2 §8.1.3.
326#[derive(Debug, Clone, PartialEq, Default)]
327#[non_exhaustive]
328pub struct BodyElement {
329    /// XML id.
330    pub xml_id: Option<String>,
331    /// XML language.
332    pub xml_lang: Option<String>,
333    /// XML space.
334    pub xml_space: Option<XmlSpace>,
335    /// `begin` time expression.
336    pub begin: Option<String>,
337    /// `dur` time expression.
338    pub dur: Option<String>,
339    /// `end` time expression.
340    pub end: Option<String>,
341    /// `timeContainer`: "par" or "seq".
342    pub time_container: Option<String>,
343    /// `region` binding.
344    pub region: Option<String>,
345    /// `style` IDREFS binding.
346    pub style: Option<String>,
347    /// `animate` IDREFS binding.
348    pub animate: Option<String>,
349    /// `condition` expression.
350    pub condition: Option<String>,
351    /// Style attributes on the body.
352    pub style_attributes: StyleAttributes,
353    /// Other attributes.
354    pub other_attributes: BTreeMap<(String, String), String>,
355    /// Child `<div>` elements.
356    pub divs: Vec<DivElement>,
357    /// Metadata children.
358    pub metadata: Vec<MetadataChild>,
359    /// Animation children.
360    pub animations: Vec<AnimationChild>,
361}
362
363/// `<div>` element — TTML2 §8.1.4.
364#[derive(Debug, Clone, PartialEq, Default)]
365#[non_exhaustive]
366pub struct DivElement {
367    /// XML id.
368    pub xml_id: Option<String>,
369    /// XML language.
370    pub xml_lang: Option<String>,
371    /// XML space.
372    pub xml_space: Option<XmlSpace>,
373    /// `begin` time expression.
374    pub begin: Option<String>,
375    /// `dur` time expression.
376    pub dur: Option<String>,
377    /// `end` time expression.
378    pub end: Option<String>,
379    /// `timeContainer`.
380    pub time_container: Option<String>,
381    /// `region` IDREF.
382    pub region: Option<String>,
383    /// `style` IDREFS.
384    pub style: Option<String>,
385    /// `animate` IDREFS.
386    pub animate: Option<String>,
387    /// `condition` expression.
388    pub condition: Option<String>,
389    /// Style attributes.
390    pub style_attributes: StyleAttributes,
391    /// Other attributes.
392    pub other_attributes: BTreeMap<(String, String), String>,
393    /// SMPTE-TT `smpte:backgroundImage` attribute.
394    pub smpte_background_image: Option<String>,
395    /// Child `<p>` elements.
396    pub paragraphs: Vec<PElement>,
397    /// Child `<image>` elements.
398    pub images: Vec<ImageElement>,
399    /// Metadata children.
400    pub metadata: Vec<MetadataChild>,
401    /// Animation children.
402    pub animations: Vec<AnimationChild>,
403}
404
405/// `<p>` element — TTML2 §8.1.5.
406#[derive(Debug, Clone, PartialEq, Default)]
407#[non_exhaustive]
408pub struct PElement {
409    /// XML id.
410    pub xml_id: Option<String>,
411    /// XML language.
412    pub xml_lang: Option<String>,
413    /// XML space.
414    pub xml_space: Option<XmlSpace>,
415    /// `begin` time expression.
416    pub begin: Option<String>,
417    /// `dur` time expression.
418    pub dur: Option<String>,
419    /// `end` time expression.
420    pub end: Option<String>,
421    /// `timeContainer`.
422    pub time_container: Option<String>,
423    /// `region` IDREF.
424    pub region: Option<String>,
425    /// `style` IDREFS.
426    pub style: Option<String>,
427    /// `animate` IDREFS.
428    pub animate: Option<String>,
429    /// `condition` expression.
430    pub condition: Option<String>,
431    /// Style attributes.
432    pub style_attributes: StyleAttributes,
433    /// Other attributes.
434    pub other_attributes: BTreeMap<(String, String), String>,
435    /// Child content: text nodes, `<span>`, `<br>`, `<image>`, `<audio>`.
436    pub content: Vec<InlineContent>,
437    /// Metadata children.
438    pub metadata: Vec<MetadataChild>,
439    /// Animation children.
440    pub animations: Vec<AnimationChild>,
441}
442
443/// `<span>` element — TTML2 §8.1.6.
444#[derive(Debug, Clone, PartialEq, Default)]
445#[non_exhaustive]
446pub struct SpanElement {
447    /// XML id.
448    pub xml_id: Option<String>,
449    /// XML language.
450    pub xml_lang: Option<String>,
451    /// XML space.
452    pub xml_space: Option<XmlSpace>,
453    /// `begin` time expression.
454    pub begin: Option<String>,
455    /// `dur` time expression.
456    pub dur: Option<String>,
457    /// `end` time expression.
458    pub end: Option<String>,
459    /// `timeContainer`.
460    pub time_container: Option<String>,
461    /// `region` IDREF.
462    pub region: Option<String>,
463    /// `style` IDREFS.
464    pub style: Option<String>,
465    /// `animate` IDREFS.
466    pub animate: Option<String>,
467    /// `condition` expression.
468    pub condition: Option<String>,
469    /// Style attributes.
470    pub style_attributes: StyleAttributes,
471    /// Other attributes.
472    pub other_attributes: BTreeMap<(String, String), String>,
473    /// Child content: text nodes, nested `<span>`, `<br>`.
474    pub content: Vec<InlineContent>,
475    /// Metadata children.
476    pub metadata: Vec<MetadataChild>,
477    /// Animation children.
478    pub animations: Vec<AnimationChild>,
479}
480
481/// `<br>` element — TTML2 §8.1.7.
482#[derive(Debug, Clone, PartialEq, Default)]
483#[non_exhaustive]
484pub struct BrElement {
485    /// XML id.
486    pub xml_id: Option<String>,
487    /// XML language.
488    pub xml_lang: Option<String>,
489    /// XML space.
490    pub xml_space: Option<XmlSpace>,
491    /// `style` IDREFS.
492    pub style: Option<String>,
493    /// `condition` expression.
494    pub condition: Option<String>,
495    /// Style attributes.
496    pub style_attributes: StyleAttributes,
497    /// Other attributes.
498    pub other_attributes: BTreeMap<(String, String), String>,
499}
500
501/// `<set>` element — TTML2 §13.1.3.
502#[derive(Debug, Clone, PartialEq, Default)]
503#[non_exhaustive]
504pub struct SetElement {
505    /// XML id.
506    pub xml_id: Option<String>,
507    /// XML language.
508    pub xml_lang: Option<String>,
509    /// XML space.
510    pub xml_space: Option<XmlSpace>,
511    /// `begin` time expression.
512    pub begin: Option<String>,
513    /// `dur` time expression.
514    pub dur: Option<String>,
515    /// `end` time expression.
516    pub end: Option<String>,
517    /// `fill` value.
518    pub fill: Option<String>,
519    /// `repeatCount` value.
520    pub repeat_count: Option<String>,
521    /// `condition` expression.
522    pub condition: Option<String>,
523    /// Style attributes.
524    pub style_attributes: StyleAttributes,
525    /// Other attributes.
526    pub other_attributes: BTreeMap<(String, String), String>,
527}
528
529/// An `<image>` element — TTML2 §9.1.5 / IMSC 1.1 §9.4.4.
530#[derive(Debug, Clone, PartialEq, Default)]
531#[non_exhaustive]
532pub struct ImageElement {
533    /// XML id.
534    pub xml_id: Option<String>,
535    /// XML language.
536    pub xml_lang: Option<String>,
537    /// XML space.
538    pub xml_space: Option<XmlSpace>,
539    /// `begin` time expression.
540    pub begin: Option<String>,
541    /// `dur` time expression.
542    pub dur: Option<String>,
543    /// `end` time expression.
544    pub end: Option<String>,
545    /// `timeContainer`.
546    pub time_container: Option<String>,
547    /// `region` IDREF.
548    pub region: Option<String>,
549    /// `style` IDREFS.
550    pub style: Option<String>,
551    /// `animate` IDREFS.
552    pub animate: Option<String>,
553    /// `condition` expression.
554    pub condition: Option<String>,
555    /// `src` URI.
556    pub src: Option<String>,
557    /// `type` MIME type.
558    pub type_: Option<String>,
559    /// `tts:extent` on the image.
560    pub tts_extent: Option<String>,
561    /// Other style attributes.
562    pub style_attributes: StyleAttributes,
563    /// Other attributes.
564    pub other_attributes: BTreeMap<(String, String), String>,
565    /// Metadata children.
566    pub metadata: Vec<MetadataChild>,
567}
568
569/// Inline content within `<p>` and `<span>`.
570#[derive(Debug, Clone, PartialEq)]
571#[non_exhaustive]
572pub enum InlineContent {
573    /// A text node (character data).
574    Text(String),
575    /// A `<span>` element.
576    Span(Box<SpanElement>),
577    /// A `<br>` element.
578    Br(Box<BrElement>),
579}
580
581/// Animation children (in body, div, p, span, region).
582#[derive(Debug, Clone, PartialEq)]
583#[non_exhaustive]
584pub enum AnimationChild {
585    /// A `<set>` element.
586    Set(SetElement),
587}
588
589/// Metadata children.
590#[derive(Debug, Clone, PartialEq)]
591#[non_exhaustive]
592pub enum MetadataChild {
593    /// A generic `<metadata>` element.
594    Metadata(MetadataElement),
595    /// `<ttm:title>` — TTML2 §14.1.8.
596    TtmTitle(TtmTextElement),
597    /// `<ttm:desc>` — TTML2 §14.1.5.
598    TtmDesc(TtmTextElement),
599    /// `<ttm:copyright>` — TTML2 §14.1.4.
600    TtmCopyright(TtmTextElement),
601    /// `<ttm:agent>` — TTML2 §14.1.3.
602    TtmAgent(TtmAgentElement),
603    /// `<ttm:item>` — TTML2 §14.1.6.
604    TtmItem(TtmItemElement),
605    /// `<ttm:name>` — TTML2 §14.1.7.
606    TtmName(TtmNameElement),
607    /// `<ebuttm:documentMetadata>` — EBU-TT-M container.
608    EbuttmDocumentMetadata(EbuttmElement),
609    /// `<ebuttm:conformsToStandard>` — EBU-TT-M profile signal.
610    EbuttmConformsToStandard(EbuttmTextElement),
611    /// `<ittm:altText>` — IMSC 1.1 §7.8.4.
612    IttmAltText(IttmAltTextElement),
613}
614
615/// A `<metadata>` element — TTML2 §14.1.1.
616#[derive(Debug, Clone, PartialEq, Default)]
617#[non_exhaustive]
618pub struct MetadataElement {
619    /// XML id.
620    pub xml_id: Option<String>,
621    /// XML language.
622    pub xml_lang: Option<String>,
623    /// XML space.
624    pub xml_space: Option<XmlSpace>,
625    /// `condition` expression.
626    pub condition: Option<String>,
627    /// Child metadata items.
628    pub children: Vec<MetadataChild>,
629}
630
631/// A text-only metadata element (`ttm:title`, `ttm:desc`, `ttm:copyright`).
632#[derive(Debug, Clone, PartialEq, Default)]
633#[non_exhaustive]
634pub struct TtmTextElement {
635    /// XML id.
636    pub xml_id: Option<String>,
637    /// XML language.
638    pub xml_lang: Option<String>,
639    /// XML space.
640    pub xml_space: Option<XmlSpace>,
641    /// `condition` expression.
642    pub condition: Option<String>,
643    /// Text content.
644    pub text: String,
645}
646
647/// `<ttm:agent>` — TTML2 §14.1.3.
648#[derive(Debug, Clone, PartialEq, Default)]
649#[non_exhaustive]
650pub struct TtmAgentElement {
651    /// XML id.
652    pub xml_id: Option<String>,
653    /// XML language.
654    pub xml_lang: Option<String>,
655    /// XML space.
656    pub xml_space: Option<XmlSpace>,
657    /// `condition`.
658    pub condition: Option<String>,
659    /// `type`: person, character, group, organization, other.
660    pub type_: Option<String>,
661    /// Child `<ttm:name>` elements.
662    pub names: Vec<TtmNameElement>,
663}
664
665/// `<ttm:name>` — TTML2 §14.1.7.
666#[derive(Debug, Clone, PartialEq, Default)]
667#[non_exhaustive]
668pub struct TtmNameElement {
669    /// XML id.
670    pub xml_id: Option<String>,
671    /// XML language.
672    pub xml_lang: Option<String>,
673    /// XML space.
674    pub xml_space: Option<XmlSpace>,
675    /// `condition`.
676    pub condition: Option<String>,
677    /// `type`: full, family, given, alias, other.
678    pub type_: Option<String>,
679    /// Text content.
680    pub text: String,
681}
682
683/// `<ttm:item>` — TTML2 §14.1.6.
684#[derive(Debug, Clone, PartialEq, Default)]
685#[non_exhaustive]
686pub struct TtmItemElement {
687    /// XML id.
688    pub xml_id: Option<String>,
689    /// XML language.
690    pub xml_lang: Option<String>,
691    /// XML space.
692    pub xml_space: Option<XmlSpace>,
693    /// `condition`.
694    pub condition: Option<String>,
695    /// `name` — either a named-item or QName.
696    pub name: Option<String>,
697    /// Text content.
698    pub text: Option<String>,
699    /// Nested `<ttm:item>` elements.
700    pub items: Vec<TtmItemElement>,
701}
702
703/// Generic EBU-TT-M element (like `<ebuttm:documentMetadata>`).
704#[derive(Debug, Clone, PartialEq, Default)]
705#[non_exhaustive]
706pub struct EbuttmElement {
707    /// Children within the EBU-TT-M element.
708    pub children: Vec<MetadataChild>,
709}
710
711/// Text-only EBU-TT-M element (like `<ebuttm:conformsToStandard>`).
712#[derive(Debug, Clone, PartialEq, Default)]
713#[non_exhaustive]
714pub struct EbuttmTextElement {
715    /// Text content.
716    pub text: String,
717}
718
719/// `<ittm:altText>` — IMSC 1.1 §7.8.4.
720#[derive(Debug, Clone, PartialEq, Default)]
721#[non_exhaustive]
722pub struct IttmAltTextElement {
723    /// XML id.
724    pub xml_id: Option<String>,
725    /// XML language.
726    pub xml_lang: Option<String>,
727    /// XML space.
728    pub xml_space: Option<XmlSpace>,
729    /// Text content.
730    pub text: String,
731}
732
733// ─── Layout elements ───────────────────────────────────────────────
734
735/// `<layout>` container — TTML2 §11.1.1.
736#[derive(Debug, Clone, PartialEq, Default)]
737#[non_exhaustive]
738pub struct LayoutElement {
739    /// XML id.
740    pub xml_id: Option<String>,
741    /// XML language.
742    pub xml_lang: Option<String>,
743    /// XML space.
744    pub xml_space: Option<XmlSpace>,
745    /// Child `<region>` elements.
746    pub regions: Vec<RegionElement>,
747}
748
749/// `<region>` element — TTML2 §11.1.2.
750#[derive(Debug, Clone, PartialEq, Default)]
751#[non_exhaustive]
752pub struct RegionElement {
753    /// XML id (required for referential binding).
754    pub xml_id: Option<String>,
755    /// XML language.
756    pub xml_lang: Option<String>,
757    /// XML space.
758    pub xml_space: Option<XmlSpace>,
759    /// `begin` time expression.
760    pub begin: Option<String>,
761    /// `dur` time expression.
762    pub dur: Option<String>,
763    /// `end` time expression.
764    pub end: Option<String>,
765    /// `timeContainer`.
766    pub time_container: Option<String>,
767    /// `style` IDREFS.
768    pub style: Option<String>,
769    /// `animate` IDREFS.
770    pub animate: Option<String>,
771    /// `condition` expression.
772    pub condition: Option<String>,
773    /// `ttm:role`.
774    pub ttm_role: Option<String>,
775    /// Style attributes on the region.
776    pub style_attributes: StyleAttributes,
777    /// Other attributes.
778    pub other_attributes: BTreeMap<(String, String), String>,
779}
780
781// ─── Styling elements ──────────────────────────────────────────────
782
783/// `<styling>` container — TTML2 §10.1.3.
784#[derive(Debug, Clone, PartialEq, Default)]
785#[non_exhaustive]
786pub struct StylingElement {
787    /// XML id.
788    pub xml_id: Option<String>,
789    /// XML language.
790    pub xml_lang: Option<String>,
791    /// XML space.
792    pub xml_space: Option<XmlSpace>,
793    /// `<initial>` elements.
794    pub initials: Vec<InitialElement>,
795    /// `<style>` elements.
796    pub styles: Vec<StyleElement>,
797}
798
799/// `<initial>` element — TTML2 §10.1.1.
800#[derive(Debug, Clone, PartialEq, Default)]
801#[non_exhaustive]
802pub struct InitialElement {
803    /// XML id.
804    pub xml_id: Option<String>,
805    /// XML language.
806    pub xml_lang: Option<String>,
807    /// XML space.
808    pub xml_space: Option<XmlSpace>,
809    /// `condition`.
810    pub condition: Option<String>,
811    /// Style attributes.
812    pub style_attributes: StyleAttributes,
813    /// Other attributes.
814    pub other_attributes: BTreeMap<(String, String), String>,
815}
816
817/// `<style>` element — TTML2 §10.1.2.
818#[derive(Debug, Clone, PartialEq, Default)]
819#[non_exhaustive]
820pub struct StyleElement {
821    /// XML id (required for referential binding).
822    pub xml_id: Option<String>,
823    /// XML language.
824    pub xml_lang: Option<String>,
825    /// XML space.
826    pub xml_space: Option<XmlSpace>,
827    /// `condition`.
828    pub condition: Option<String>,
829    /// `style` (IDREFS, for chaining).
830    pub style: Option<String>,
831    /// Style attributes.
832    pub style_attributes: StyleAttributes,
833    /// Other attributes.
834    pub other_attributes: BTreeMap<(String, String), String>,
835}
836
837// ─── Style attributes ──────────────────────────────────────────────
838
839/// All 52 TTML2 style properties (and IMSC extensions) collected in one struct.
840///
841/// Each field is `Option<String>` — `None` means not specified. Style property
842/// names follow `ttml2-syntax.md` §3.5 (56 properties + IMSC extensions).
843#[derive(Debug, Clone, PartialEq, Default)]
844#[non_exhaustive]
845pub struct StyleAttributes {
846    /// `tts:backgroundColor` — TTML2 §10.2.4.
847    pub tts_background_color: Option<String>,
848    /// `tts:backgroundClip` — TTML2 §10.2.2.
849    pub tts_background_clip: Option<String>,
850    /// `tts:backgroundExtent` — TTML2 §10.2.5.
851    pub tts_background_extent: Option<String>,
852    /// `tts:backgroundImage` — TTML2 §10.2.6.
853    pub tts_background_image: Option<String>,
854    /// `tts:backgroundOrigin` — TTML2 §10.2.7.
855    pub tts_background_origin: Option<String>,
856    /// `tts:backgroundPosition` — TTML2 §10.2.8.
857    pub tts_background_position: Option<String>,
858    /// `tts:backgroundRepeat` — TTML2 §10.2.9.
859    pub tts_background_repeat: Option<String>,
860    /// `tts:border` — TTML2 §10.2.10.
861    pub tts_border: Option<String>,
862    /// `tts:bpd` — TTML2 §10.2.11.
863    pub tts_bpd: Option<String>,
864    /// `tts:color` — TTML2 §10.2.12.
865    pub tts_color: Option<String>,
866    /// `tts:direction` — TTML2 §10.2.13.
867    pub tts_direction: Option<String>,
868    /// `tts:disparity` — TTML2 §10.2.14.
869    pub tts_disparity: Option<String>,
870    /// `tts:display` — TTML2 §10.2.15.
871    pub tts_display: Option<String>,
872    /// `tts:displayAlign` — TTML2 §10.2.16.
873    pub tts_display_align: Option<String>,
874    /// `tts:extent` — TTML2 §10.2.17.
875    pub tts_extent: Option<String>,
876    /// `tts:fontFamily` — TTML2 §10.2.18.
877    pub tts_font_family: Option<String>,
878    /// `tts:fontKerning` — TTML2 §10.2.19.
879    pub tts_font_kerning: Option<String>,
880    /// `tts:fontSelectionStrategy` — TTML2 §10.2.20.
881    pub tts_font_selection_strategy: Option<String>,
882    /// `tts:fontShear` — TTML2 §10.2.21.
883    pub tts_font_shear: Option<String>,
884    /// `tts:fontSize` — TTML2 §10.2.22.
885    pub tts_font_size: Option<String>,
886    /// `tts:fontStyle` — TTML2 §10.2.23.
887    pub tts_font_style: Option<String>,
888    /// `tts:fontVariant` — TTML2 §10.2.24.
889    pub tts_font_variant: Option<String>,
890    /// `tts:fontWeight` — TTML2 §10.2.25.
891    pub tts_font_weight: Option<String>,
892    /// `tts:ipd` — TTML2 §10.2.26.
893    pub tts_ipd: Option<String>,
894    /// `tts:letterSpacing` — TTML2 §10.2.27.
895    pub tts_letter_spacing: Option<String>,
896    /// `tts:lineHeight` — TTML2 §10.2.28.
897    pub tts_line_height: Option<String>,
898    /// `tts:lineShear` — TTML2 §10.2.29.
899    pub tts_line_shear: Option<String>,
900    /// `tts:luminanceGain` — TTML2 §10.2.30.
901    pub tts_luminance_gain: Option<String>,
902    /// `tts:opacity` — TTML2 §10.2.31.
903    pub tts_opacity: Option<String>,
904    /// `tts:origin` — TTML2 §10.2.32.
905    pub tts_origin: Option<String>,
906    /// `tts:overflow` — TTML2 §10.2.33.
907    pub tts_overflow: Option<String>,
908    /// `tts:padding` — TTML2 §10.2.34.
909    pub tts_padding: Option<String>,
910    /// `tts:position` — TTML2 §10.2.35.
911    pub tts_position: Option<String>,
912    /// `tts:ruby` — TTML2 §10.2.36.
913    pub tts_ruby: Option<String>,
914    /// `tts:rubyAlign` — TTML2 §10.2.37.
915    pub tts_ruby_align: Option<String>,
916    /// `tts:rubyPosition` — TTML2 §10.2.38.
917    pub tts_ruby_position: Option<String>,
918    /// `tts:rubyReserve` — TTML2 §10.2.39.
919    pub tts_ruby_reserve: Option<String>,
920    /// `tts:shear` — TTML2 §10.2.40.
921    pub tts_shear: Option<String>,
922    /// `tts:showBackground` — TTML2 §10.2.41.
923    pub tts_show_background: Option<String>,
924    /// `tts:textAlign` — TTML2 §10.2.42.
925    pub tts_text_align: Option<String>,
926    /// `tts:textCombine` — TTML2 §10.2.43.
927    pub tts_text_combine: Option<String>,
928    /// `tts:textDecoration` — TTML2 §10.2.44.
929    pub tts_text_decoration: Option<String>,
930    /// `tts:textEmphasis` — TTML2 §10.2.45.
931    pub tts_text_emphasis: Option<String>,
932    /// `tts:textOrientation` — TTML2 §10.2.46.
933    pub tts_text_orientation: Option<String>,
934    /// `tts:textOutline` — TTML2 §10.2.47.
935    pub tts_text_outline: Option<String>,
936    /// `tts:textShadow` — TTML2 §10.2.48.
937    pub tts_text_shadow: Option<String>,
938    /// `tts:unicodeBidi` — TTML2 §10.2.49.
939    pub tts_unicode_bidi: Option<String>,
940    /// `tts:visibility` — TTML2 §10.2.50.
941    pub tts_visibility: Option<String>,
942    /// `tts:wrapOption` — TTML2 §10.2.51.
943    pub tts_wrap_option: Option<String>,
944    /// `tts:writingMode` — TTML2 §10.2.52.
945    pub tts_writing_mode: Option<String>,
946    /// `tts:zIndex` — TTML2 §10.2.53.
947    pub tts_z_index: Option<String>,
948    /// `tta:gain` — TTML2 §10.2.54.
949    pub tta_gain: Option<String>,
950    /// `tta:pan` — TTML2 §10.2.55.
951    pub tta_pan: Option<String>,
952    /// `tta:pitch` — TTML2 §10.2.56.
953    pub tta_pitch: Option<String>,
954    /// `tta:speak` — TTML2 §10.2.57.
955    pub tta_speak: Option<String>,
956    /// `itts:forcedDisplay` — IMSC 1.1 §7.8.3.
957    pub itts_forced_display: Option<String>,
958    /// `itts:fillLineGap` — IMSC 1.1 §7.8.6.
959    pub itts_fill_line_gap: Option<String>,
960    /// `ebutts:linePadding` — EBU-TT-D style extension.
961    pub ebutts_line_padding: Option<String>,
962    /// `ebutts:multiRowAlign` — EBU-TT-D style extension.
963    pub ebutts_multi_row_align: Option<String>,
964}
965
966/// `xml:space` values.
967#[derive(Debug, Clone, Copy, PartialEq, Eq)]
968#[non_exhaustive]
969pub enum XmlSpace {
970    /// Default whitespace handling.
971    Default,
972    /// Preserve whitespace.
973    Preserve,
974}
975
976impl XmlSpace {
977    /// Label for the #204 convention.
978    pub fn name(&self) -> &'static str {
979        match self {
980            XmlSpace::Default => "default",
981            XmlSpace::Preserve => "preserve",
982        }
983    }
984}
985
986broadcast_common::impl_spec_display!(XmlSpace);
987
988// ─── XML Parsing Helpers ───────────────────────────────────────────
989
990// Removed: resolve_ns unused, has_itts unused
991
992/// Get the prefixed name for an attribute value lookup in the document context.
993fn attribute_value<'a>(node: &roxmltree::Node<'a, 'a>, ns: &str, local: &str) -> Option<&'a str> {
994    // Try all attributes on the node matching ns + local_name
995    for attr in node.attributes() {
996        if attr.namespace() == Some(ns) && attr.name() == local {
997            return Some(attr.value());
998        }
999    }
1000    None
1001}
1002
1003/// Get all non-TT-namespace attributes for generic passthrough.
1004fn other_attributes(node: &roxmltree::Node<'_, '_>) -> BTreeMap<(String, String), String> {
1005    let known_nses = &[
1006        NS_TT,
1007        NS_TTP,
1008        NS_TTS,
1009        NS_TTA,
1010        NS_TTM,
1011        NS_TT_PROFILE,
1012        NS_ITTS,
1013        NS_ITTP,
1014        NS_ITTM,
1015        NS_EBUTTS,
1016        NS_EBUTTM,
1017        NS_SMPTE,
1018        NS_XML,
1019        "", // no-namespace attributes like begin/end/dur
1020    ];
1021    let mut map = BTreeMap::new();
1022    for attr in node.attributes() {
1023        let ns = attr.namespace().unwrap_or("");
1024        let local = attr.name();
1025        if !known_nses.contains(&ns) {
1026            map.insert(
1027                (ns.to_string(), local.to_string()),
1028                attr.value().to_string(),
1029            );
1030        }
1031    }
1032    map
1033}
1034
1035// ─── XML Parsing Functions ─────────────────────────────────────────
1036
1037/// Parse the root `<tt>` element from a roxmltree node.
1038fn parse_tt_element(node: roxmltree::Node<'_, '_>) -> Result<TtElement> {
1039    let mut head = None;
1040    let mut body = None;
1041
1042    for child in node.children() {
1043        if !child.is_element() {
1044            continue;
1045        }
1046        let name = child.tag_name().name();
1047        let ns = child.tag_name().namespace();
1048
1049        match (name, ns) {
1050            ("head", Some(NS_TT)) => {
1051                head = Some(parse_head_element(child)?);
1052            }
1053            ("body", Some(NS_TT)) => {
1054                body = Some(parse_body_element(child)?);
1055            }
1056            _ => {
1057                // Skip unknown elements in the TT namespace (foreign elements allowed per §7.2)
1058            }
1059        }
1060    }
1061
1062    // Parse the style attributes on tt
1063    let _ = parse_style_attributes(node);
1064
1065    Ok(TtElement {
1066        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1067        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1068        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1069        ttp_time_base: attribute_value(&node, NS_TTP, "timeBase").map(|s| s.to_string()),
1070        ttp_frame_rate: attribute_value(&node, NS_TTP, "frameRate").map(|s| s.to_string()),
1071        ttp_frame_rate_multiplier: attribute_value(&node, NS_TTP, "frameRateMultiplier")
1072            .map(|s| s.to_string()),
1073        ttp_tick_rate: attribute_value(&node, NS_TTP, "tickRate").map(|s| s.to_string()),
1074        ttp_sub_frame_rate: attribute_value(&node, NS_TTP, "subFrameRate").map(|s| s.to_string()),
1075        ttp_drop_mode: attribute_value(&node, NS_TTP, "dropMode").map(|s| s.to_string()),
1076        ttp_marker_mode: attribute_value(&node, NS_TTP, "markerMode").map(|s| s.to_string()),
1077        ttp_clock_mode: attribute_value(&node, NS_TTP, "clockMode").map(|s| s.to_string()),
1078        ttp_cell_resolution: attribute_value(&node, NS_TTP, "cellResolution")
1079            .map(|s| s.to_string()),
1080        ttp_pixel_aspect_ratio: attribute_value(&node, NS_TTP, "pixelAspectRatio")
1081            .map(|s| s.to_string()),
1082        ttp_display_aspect_ratio: attribute_value(&node, NS_TTP, "displayAspectRatio")
1083            .map(|s| s.to_string()),
1084        ttp_profile: attribute_value(&node, NS_TTP, "profile").map(|s| s.to_string()),
1085        ttp_content_profiles: attribute_value(&node, NS_TTP, "contentProfiles")
1086            .map(|s| s.to_string()),
1087        ttp_content_profile_combination: attribute_value(
1088            &node,
1089            NS_TTP,
1090            "contentProfileCombination",
1091        )
1092        .map(|s| s.to_string()),
1093        ttp_processor_profiles: attribute_value(&node, NS_TTP, "processorProfiles")
1094            .map(|s| s.to_string()),
1095        ttp_processor_profile_combination: attribute_value(
1096            &node,
1097            NS_TTP,
1098            "processorProfileCombination",
1099        )
1100        .map(|s| s.to_string()),
1101        ttp_infer_processor_profile_method: attribute_value(
1102            &node,
1103            NS_TTP,
1104            "inferProcessorProfileMethod",
1105        )
1106        .map(|s| s.to_string()),
1107        ttp_infer_processor_profile_source: attribute_value(
1108            &node,
1109            NS_TTP,
1110            "inferProcessorProfileSource",
1111        )
1112        .map(|s| s.to_string()),
1113        ttp_permit_feature_narrowing: attribute_value(&node, NS_TTP, "permitFeatureNarrowing")
1114            .map(|s| s.to_string()),
1115        ttp_permit_feature_widening: attribute_value(&node, NS_TTP, "permitFeatureWidening")
1116            .map(|s| s.to_string()),
1117        ttp_validation: attribute_value(&node, NS_TTP, "validation").map(|s| s.to_string()),
1118        ttp_validation_action: attribute_value(&node, NS_TTP, "validationAction")
1119            .map(|s| s.to_string()),
1120        tts_extent: attribute_value(&node, NS_TTS, "extent").map(|s| s.to_string()),
1121        ittp_active_area: attribute_value(&node, NS_ITTP, "activeArea").map(|s| s.to_string()),
1122        ittp_aspect_ratio: attribute_value(&node, NS_ITTP, "aspectRatio").map(|s| s.to_string()),
1123        ittp_progressively_decodable: attribute_value(&node, NS_ITTP, "progressivelyDecodable")
1124            .map(|s| s.to_string()),
1125        other_attributes: other_attributes(&node),
1126        head,
1127        body,
1128        text: node.text().map(|s| s.to_string()),
1129    })
1130}
1131
1132fn parse_head_element(node: roxmltree::Node<'_, '_>) -> Result<HeadElement> {
1133    let mut metadata = Vec::new();
1134    let mut styling = None;
1135    let mut layout = None;
1136
1137    // Also collect metadata from top-level
1138    for child in node.children() {
1139        if !child.is_element() {
1140            continue;
1141        }
1142        let name = child.tag_name().name();
1143        let ns = child.tag_name().namespace();
1144
1145        match (name, ns) {
1146            ("metadata", Some(NS_TT)) => {
1147                metadata.push(MetadataChild::Metadata(parse_metadata_element(child)?));
1148            }
1149            ("title", Some(NS_TTM)) => {
1150                metadata.push(MetadataChild::TtmTitle(parse_ttm_text(child)?));
1151            }
1152            ("desc", Some(NS_TTM)) => {
1153                metadata.push(MetadataChild::TtmDesc(parse_ttm_text(child)?));
1154            }
1155            ("copyright", Some(NS_TTM)) => {
1156                metadata.push(MetadataChild::TtmCopyright(parse_ttm_text(child)?));
1157            }
1158            ("agent", Some(NS_TTM)) => {
1159                metadata.push(MetadataChild::TtmAgent(parse_ttm_agent(child)?));
1160            }
1161            ("item", Some(NS_TTM)) => {
1162                metadata.push(MetadataChild::TtmItem(parse_ttm_item(child)?));
1163            }
1164            ("name", Some(NS_TTM)) => {
1165                metadata.push(MetadataChild::TtmName(parse_ttm_name(child)?));
1166            }
1167            ("documentMetadata", Some(NS_EBUTTM)) => {
1168                metadata.push(MetadataChild::EbuttmDocumentMetadata(parse_ebuttm_element(
1169                    child,
1170                )?));
1171            }
1172            ("conformsToStandard", Some(NS_EBUTTM)) => {
1173                metadata.push(MetadataChild::EbuttmConformsToStandard(parse_ebuttm_text(
1174                    child,
1175                )?));
1176            }
1177            ("altText", Some(NS_ITTM)) => {
1178                metadata.push(MetadataChild::IttmAltText(parse_ittm_alt_text(child)?));
1179            }
1180            ("styling", Some(NS_TT)) => {
1181                styling = Some(parse_styling_element(child)?);
1182            }
1183            ("layout", Some(NS_TT)) => {
1184                layout = Some(parse_layout_element(child)?);
1185            }
1186            _ => {}
1187        }
1188    }
1189
1190    Ok(HeadElement {
1191        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1192        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1193        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1194        metadata,
1195        styling,
1196        layout,
1197    })
1198}
1199
1200fn parse_body_element(node: roxmltree::Node<'_, '_>) -> Result<BodyElement> {
1201    let mut divs = Vec::new();
1202    let mut metadata = Vec::new();
1203    let mut animations = Vec::new();
1204
1205    for child in node.children() {
1206        if !child.is_element() {
1207            continue;
1208        }
1209        let name = child.tag_name().name();
1210        let ns = child.tag_name().namespace();
1211
1212        match (name, ns) {
1213            ("div", Some(NS_TT)) => {
1214                divs.push(parse_div_element(child)?);
1215            }
1216            ("metadata", Some(NS_TT)) => {
1217                metadata.push(MetadataChild::Metadata(parse_metadata_element(child)?));
1218            }
1219            ("title", Some(NS_TTM)) => {
1220                metadata.push(MetadataChild::TtmTitle(parse_ttm_text(child)?));
1221            }
1222            ("desc", Some(NS_TTM)) => {
1223                metadata.push(MetadataChild::TtmDesc(parse_ttm_text(child)?));
1224            }
1225            ("copyright", Some(NS_TTM)) => {
1226                metadata.push(MetadataChild::TtmCopyright(parse_ttm_text(child)?));
1227            }
1228            ("documentMetadata", Some(NS_EBUTTM)) => {
1229                metadata.push(MetadataChild::EbuttmDocumentMetadata(parse_ebuttm_element(
1230                    child,
1231                )?));
1232            }
1233            ("conformsToStandard", Some(NS_EBUTTM)) => {
1234                metadata.push(MetadataChild::EbuttmConformsToStandard(parse_ebuttm_text(
1235                    child,
1236                )?));
1237            }
1238            ("altText", Some(NS_ITTM)) => {
1239                metadata.push(MetadataChild::IttmAltText(parse_ittm_alt_text(child)?));
1240            }
1241            ("set", Some(NS_TT)) => {
1242                animations.push(AnimationChild::Set(parse_set_element(child)?));
1243            }
1244            _ => {}
1245        }
1246    }
1247
1248    let style_attrs = parse_style_attributes(node);
1249
1250    Ok(BodyElement {
1251        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1252        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1253        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1254        begin: node.attribute("begin").map(|s| s.to_string()),
1255        dur: node.attribute("dur").map(|s| s.to_string()),
1256        end: node.attribute("end").map(|s| s.to_string()),
1257        time_container: node.attribute("timeContainer").map(|s| s.to_string()),
1258        region: node.attribute("region").map(|s| s.to_string()),
1259        style: node.attribute("style").map(|s| s.to_string()),
1260        animate: node.attribute("animate").map(|s| s.to_string()),
1261        condition: node.attribute("condition").map(|s| s.to_string()),
1262        style_attributes: style_attrs,
1263        other_attributes: other_attributes(&node),
1264        divs,
1265        metadata,
1266        animations,
1267    })
1268}
1269
1270fn parse_div_element(node: roxmltree::Node<'_, '_>) -> Result<DivElement> {
1271    let mut paragraphs = Vec::new();
1272    let mut images = Vec::new();
1273    let mut metadata = Vec::new();
1274    let mut animations = Vec::new();
1275
1276    for child in node.children() {
1277        if !child.is_element() {
1278            continue;
1279        }
1280        let name = child.tag_name().name();
1281        let ns = child.tag_name().namespace();
1282
1283        match (name, ns) {
1284            ("p", Some(NS_TT)) => {
1285                paragraphs.push(parse_p_element(child)?);
1286            }
1287            ("image", Some(NS_TT)) => {
1288                images.push(parse_image_element(child)?);
1289            }
1290            ("metadata", Some(NS_TT)) => {
1291                metadata.push(MetadataChild::Metadata(parse_metadata_element(child)?));
1292            }
1293            ("altText", Some(NS_ITTM)) => {
1294                metadata.push(MetadataChild::IttmAltText(parse_ittm_alt_text(child)?));
1295            }
1296            ("set", Some(NS_TT)) => {
1297                animations.push(AnimationChild::Set(parse_set_element(child)?));
1298            }
1299            _ => {}
1300        }
1301    }
1302
1303    let style_attrs = parse_style_attributes(node);
1304
1305    Ok(DivElement {
1306        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1307        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1308        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1309        begin: node.attribute("begin").map(|s| s.to_string()),
1310        dur: node.attribute("dur").map(|s| s.to_string()),
1311        end: node.attribute("end").map(|s| s.to_string()),
1312        time_container: node.attribute("timeContainer").map(|s| s.to_string()),
1313        region: node.attribute("region").map(|s| s.to_string()),
1314        style: node.attribute("style").map(|s| s.to_string()),
1315        animate: node.attribute("animate").map(|s| s.to_string()),
1316        condition: node.attribute("condition").map(|s| s.to_string()),
1317        style_attributes: style_attrs,
1318        other_attributes: other_attributes(&node),
1319        smpte_background_image: attribute_value(&node, NS_SMPTE, "backgroundImage")
1320            .map(|s| s.to_string()),
1321        paragraphs,
1322        images,
1323        metadata,
1324        animations,
1325    })
1326}
1327
1328fn parse_p_element(node: roxmltree::Node<'_, '_>) -> Result<PElement> {
1329    let mut content: Vec<InlineContent> = Vec::new();
1330    let mut metadata = Vec::new();
1331    let mut animations = Vec::new();
1332
1333    for child in node.children() {
1334        if child.is_text() {
1335            let text = child.text().unwrap_or("");
1336            if !text.is_empty() {
1337                if let Some(last) = content.last_mut()
1338                    && let InlineContent::Text(t) = last
1339                {
1340                    t.push_str(text);
1341                    continue;
1342                }
1343                content.push(InlineContent::Text(text.to_string()));
1344            }
1345        } else if child.is_element() {
1346            let name = child.tag_name().name();
1347            let ns = child.tag_name().namespace();
1348
1349            match (name, ns) {
1350                ("span", Some(NS_TT)) => {
1351                    content.push(InlineContent::Span(Box::new(parse_span_element(child)?)));
1352                }
1353                ("br", Some(NS_TT)) => {
1354                    content.push(InlineContent::Br(Box::new(parse_br_element(child)?)));
1355                }
1356                ("metadata", Some(NS_TT)) => {
1357                    metadata.push(MetadataChild::Metadata(parse_metadata_element(child)?));
1358                }
1359                ("set", Some(NS_TT)) => {
1360                    animations.push(AnimationChild::Set(parse_set_element(child)?));
1361                }
1362                _ => {}
1363            }
1364        }
1365    }
1366
1367    let style_attrs = parse_style_attributes(node);
1368
1369    Ok(PElement {
1370        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1371        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1372        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1373        begin: node.attribute("begin").map(|s| s.to_string()),
1374        dur: node.attribute("dur").map(|s| s.to_string()),
1375        end: node.attribute("end").map(|s| s.to_string()),
1376        time_container: node.attribute("timeContainer").map(|s| s.to_string()),
1377        region: node.attribute("region").map(|s| s.to_string()),
1378        style: node.attribute("style").map(|s| s.to_string()),
1379        animate: node.attribute("animate").map(|s| s.to_string()),
1380        condition: node.attribute("condition").map(|s| s.to_string()),
1381        style_attributes: style_attrs,
1382        other_attributes: other_attributes(&node),
1383        content,
1384        metadata,
1385        animations,
1386    })
1387}
1388
1389fn parse_span_element(node: roxmltree::Node<'_, '_>) -> Result<SpanElement> {
1390    let mut content: Vec<InlineContent> = Vec::new();
1391    let mut metadata = Vec::new();
1392    let mut animations = Vec::new();
1393
1394    for child in node.children() {
1395        if child.is_text() {
1396            let text = child.text().unwrap_or("");
1397            if !text.is_empty() {
1398                if let Some(last) = content.last_mut()
1399                    && let InlineContent::Text(t) = last
1400                {
1401                    t.push_str(text);
1402                    continue;
1403                }
1404                content.push(InlineContent::Text(text.to_string()));
1405            }
1406        } else if child.is_element() {
1407            let name = child.tag_name().name();
1408            let ns = child.tag_name().namespace();
1409
1410            match (name, ns) {
1411                ("span", Some(NS_TT)) => {
1412                    content.push(InlineContent::Span(Box::new(parse_span_element(child)?)));
1413                }
1414                ("br", Some(NS_TT)) => {
1415                    content.push(InlineContent::Br(Box::new(parse_br_element(child)?)));
1416                }
1417                ("metadata", Some(NS_TT)) => {
1418                    metadata.push(MetadataChild::Metadata(parse_metadata_element(child)?));
1419                }
1420                ("set", Some(NS_TT)) => {
1421                    animations.push(AnimationChild::Set(parse_set_element(child)?));
1422                }
1423                _ => {}
1424            }
1425        }
1426    }
1427
1428    let style_attrs = parse_style_attributes(node);
1429
1430    Ok(SpanElement {
1431        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1432        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1433        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1434        begin: node.attribute("begin").map(|s| s.to_string()),
1435        dur: node.attribute("dur").map(|s| s.to_string()),
1436        end: node.attribute("end").map(|s| s.to_string()),
1437        time_container: node.attribute("timeContainer").map(|s| s.to_string()),
1438        region: node.attribute("region").map(|s| s.to_string()),
1439        style: node.attribute("style").map(|s| s.to_string()),
1440        animate: node.attribute("animate").map(|s| s.to_string()),
1441        condition: node.attribute("condition").map(|s| s.to_string()),
1442        style_attributes: style_attrs,
1443        other_attributes: other_attributes(&node),
1444        content,
1445        metadata,
1446        animations,
1447    })
1448}
1449
1450fn parse_br_element(node: roxmltree::Node<'_, '_>) -> Result<BrElement> {
1451    let style_attrs = parse_style_attributes(node);
1452    Ok(BrElement {
1453        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1454        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1455        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1456        style: node.attribute("style").map(|s| s.to_string()),
1457        condition: node.attribute("condition").map(|s| s.to_string()),
1458        style_attributes: style_attrs,
1459        other_attributes: other_attributes(&node),
1460    })
1461}
1462
1463fn parse_set_element(node: roxmltree::Node<'_, '_>) -> Result<SetElement> {
1464    let style_attrs = parse_style_attributes(node);
1465    Ok(SetElement {
1466        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1467        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1468        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1469        begin: node.attribute("begin").map(|s| s.to_string()),
1470        dur: node.attribute("dur").map(|s| s.to_string()),
1471        end: node.attribute("end").map(|s| s.to_string()),
1472        fill: node.attribute("fill").map(|s| s.to_string()),
1473        repeat_count: node.attribute("repeatCount").map(|s| s.to_string()),
1474        condition: node.attribute("condition").map(|s| s.to_string()),
1475        style_attributes: style_attrs,
1476        other_attributes: other_attributes(&node),
1477    })
1478}
1479
1480fn parse_image_element(node: roxmltree::Node<'_, '_>) -> Result<ImageElement> {
1481    let mut metadata = Vec::new();
1482
1483    for child in node.children() {
1484        if !child.is_element() {
1485            continue;
1486        }
1487        let name = child.tag_name().name();
1488        let ns = child.tag_name().namespace();
1489
1490        match (name, ns) {
1491            ("metadata", Some(NS_TT)) => {
1492                metadata.push(MetadataChild::Metadata(parse_metadata_element(child)?));
1493            }
1494            ("altText", Some(NS_ITTM)) => {
1495                metadata.push(MetadataChild::IttmAltText(parse_ittm_alt_text(child)?));
1496            }
1497            _ => {}
1498        }
1499    }
1500
1501    let style_attrs = parse_style_attributes(node);
1502
1503    Ok(ImageElement {
1504        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1505        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1506        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1507        begin: node.attribute("begin").map(|s| s.to_string()),
1508        dur: node.attribute("dur").map(|s| s.to_string()),
1509        end: node.attribute("end").map(|s| s.to_string()),
1510        time_container: node.attribute("timeContainer").map(|s| s.to_string()),
1511        region: node.attribute("region").map(|s| s.to_string()),
1512        style: node.attribute("style").map(|s| s.to_string()),
1513        animate: node.attribute("animate").map(|s| s.to_string()),
1514        condition: node.attribute("condition").map(|s| s.to_string()),
1515        src: node.attribute("src").map(|s| s.to_string()),
1516        type_: node.attribute("type").map(|s| s.to_string()),
1517        tts_extent: attribute_value(&node, NS_TTS, "extent").map(|s| s.to_string()),
1518        style_attributes: style_attrs,
1519        other_attributes: other_attributes(&node),
1520        metadata,
1521    })
1522}
1523
1524fn parse_metadata_element(node: roxmltree::Node<'_, '_>) -> Result<MetadataElement> {
1525    let mut children = Vec::new();
1526
1527    for child in node.children() {
1528        if !child.is_element() {
1529            continue;
1530        }
1531        let name = child.tag_name().name();
1532        let ns = child.tag_name().namespace();
1533
1534        match (name, ns) {
1535            ("metadata", Some(NS_TT)) => {
1536                children.push(MetadataChild::Metadata(parse_metadata_element(child)?));
1537            }
1538            ("title", Some(NS_TTM)) => {
1539                children.push(MetadataChild::TtmTitle(parse_ttm_text(child)?));
1540            }
1541            ("desc", Some(NS_TTM)) => {
1542                children.push(MetadataChild::TtmDesc(parse_ttm_text(child)?));
1543            }
1544            ("copyright", Some(NS_TTM)) => {
1545                children.push(MetadataChild::TtmCopyright(parse_ttm_text(child)?));
1546            }
1547            ("agent", Some(NS_TTM)) => {
1548                children.push(MetadataChild::TtmAgent(parse_ttm_agent(child)?));
1549            }
1550            ("item", Some(NS_TTM)) => {
1551                children.push(MetadataChild::TtmItem(parse_ttm_item(child)?));
1552            }
1553            ("name", Some(NS_TTM)) => {
1554                children.push(MetadataChild::TtmName(parse_ttm_name(child)?));
1555            }
1556            ("documentMetadata", Some(NS_EBUTTM)) => {
1557                children.push(MetadataChild::EbuttmDocumentMetadata(parse_ebuttm_element(
1558                    child,
1559                )?));
1560            }
1561            ("conformsToStandard", Some(NS_EBUTTM)) => {
1562                children.push(MetadataChild::EbuttmConformsToStandard(parse_ebuttm_text(
1563                    child,
1564                )?));
1565            }
1566            ("altText", Some(NS_ITTM)) => {
1567                children.push(MetadataChild::IttmAltText(parse_ittm_alt_text(child)?));
1568            }
1569            _ => {}
1570        }
1571    }
1572
1573    Ok(MetadataElement {
1574        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1575        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1576        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1577        condition: node.attribute("condition").map(|s| s.to_string()),
1578        children,
1579    })
1580}
1581
1582fn parse_ttm_text(node: roxmltree::Node<'_, '_>) -> Result<TtmTextElement> {
1583    let text = node.text().unwrap_or("").to_string();
1584    Ok(TtmTextElement {
1585        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1586        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1587        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1588        condition: node.attribute("condition").map(|s| s.to_string()),
1589        text,
1590    })
1591}
1592
1593fn parse_ttm_agent(node: roxmltree::Node<'_, '_>) -> Result<TtmAgentElement> {
1594    let mut names = Vec::new();
1595    for child in node.children() {
1596        if child.is_element()
1597            && child.tag_name().name() == "name"
1598            && child.tag_name().namespace() == Some(NS_TTM)
1599        {
1600            names.push(parse_ttm_name(child)?);
1601        }
1602    }
1603    Ok(TtmAgentElement {
1604        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1605        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1606        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1607        condition: node.attribute("condition").map(|s| s.to_string()),
1608        type_: node.attribute("type").map(|s| s.to_string()),
1609        names,
1610    })
1611}
1612
1613fn parse_ttm_name(node: roxmltree::Node<'_, '_>) -> Result<TtmNameElement> {
1614    let text = node.text().unwrap_or("").to_string();
1615    Ok(TtmNameElement {
1616        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1617        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1618        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1619        condition: node.attribute("condition").map(|s| s.to_string()),
1620        type_: node.attribute("type").map(|s| s.to_string()),
1621        text,
1622    })
1623}
1624
1625fn parse_ttm_item(node: roxmltree::Node<'_, '_>) -> Result<TtmItemElement> {
1626    let mut items = Vec::new();
1627    let mut text_parts = String::new();
1628
1629    for child in node.children() {
1630        if child.is_text() {
1631            if let Some(t) = child.text() {
1632                text_parts.push_str(t);
1633            }
1634        } else if child.is_element()
1635            && child.tag_name().name() == "item"
1636            && child.tag_name().namespace() == Some(NS_TTM)
1637        {
1638            items.push(parse_ttm_item(child)?);
1639        }
1640    }
1641
1642    Ok(TtmItemElement {
1643        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1644        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1645        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1646        condition: node.attribute("condition").map(|s| s.to_string()),
1647        name: node.attribute("name").map(|s| s.to_string()),
1648        text: if text_parts.is_empty() {
1649            None
1650        } else {
1651            Some(text_parts)
1652        },
1653        items,
1654    })
1655}
1656
1657fn parse_ittm_alt_text(node: roxmltree::Node<'_, '_>) -> Result<IttmAltTextElement> {
1658    let text = node.text().unwrap_or("").to_string();
1659    Ok(IttmAltTextElement {
1660        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1661        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1662        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1663        text,
1664    })
1665}
1666
1667fn parse_ebuttm_element(node: roxmltree::Node<'_, '_>) -> Result<EbuttmElement> {
1668    let mut children = Vec::new();
1669    for child in node.children() {
1670        if !child.is_element() {
1671            continue;
1672        }
1673        let name = child.tag_name().name();
1674        let ns = child.tag_name().namespace();
1675
1676        if name == "conformsToStandard" && ns == Some(NS_EBUTTM) {
1677            children.push(MetadataChild::EbuttmConformsToStandard(parse_ebuttm_text(
1678                child,
1679            )?));
1680        }
1681    }
1682    Ok(EbuttmElement { children })
1683}
1684
1685fn parse_ebuttm_text(node: roxmltree::Node<'_, '_>) -> Result<EbuttmTextElement> {
1686    let text = node.text().unwrap_or("").to_string();
1687    Ok(EbuttmTextElement { text })
1688}
1689
1690fn parse_styling_element(node: roxmltree::Node<'_, '_>) -> Result<StylingElement> {
1691    let mut initials = Vec::new();
1692    let mut styles = Vec::new();
1693
1694    for child in node.children() {
1695        if !child.is_element() {
1696            continue;
1697        }
1698        let name = child.tag_name().name();
1699        let ns = child.tag_name().namespace();
1700
1701        match (name, ns) {
1702            ("initial", Some(NS_TT)) => {
1703                initials.push(parse_initial_element(child)?);
1704            }
1705            ("style", Some(NS_TT)) => {
1706                styles.push(parse_style_element(child)?);
1707            }
1708            _ => {}
1709        }
1710    }
1711
1712    Ok(StylingElement {
1713        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1714        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1715        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1716        initials,
1717        styles,
1718    })
1719}
1720
1721fn parse_initial_element(node: roxmltree::Node<'_, '_>) -> Result<InitialElement> {
1722    Ok(InitialElement {
1723        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1724        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1725        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1726        condition: node.attribute("condition").map(|s| s.to_string()),
1727        style_attributes: parse_style_attributes(node),
1728        other_attributes: other_attributes(&node),
1729    })
1730}
1731
1732fn parse_style_element(node: roxmltree::Node<'_, '_>) -> Result<StyleElement> {
1733    Ok(StyleElement {
1734        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1735        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1736        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1737        condition: node.attribute("condition").map(|s| s.to_string()),
1738        style: node.attribute("style").map(|s| s.to_string()),
1739        style_attributes: parse_style_attributes(node),
1740        other_attributes: other_attributes(&node),
1741    })
1742}
1743
1744fn parse_layout_element(node: roxmltree::Node<'_, '_>) -> Result<LayoutElement> {
1745    let mut regions = Vec::new();
1746    for child in node.children() {
1747        if child.is_element()
1748            && child.tag_name().name() == "region"
1749            && child.tag_name().namespace() == Some(NS_TT)
1750        {
1751            regions.push(parse_region_element(child)?);
1752        }
1753    }
1754
1755    Ok(LayoutElement {
1756        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1757        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1758        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1759        regions,
1760    })
1761}
1762
1763fn parse_region_element(node: roxmltree::Node<'_, '_>) -> Result<RegionElement> {
1764    let style_attrs = parse_style_attributes(node);
1765
1766    Ok(RegionElement {
1767        xml_id: attribute_value(&node, NS_XML, "id").map(|s| s.to_string()),
1768        xml_lang: attribute_value(&node, NS_XML, "lang").map(|s| s.to_string()),
1769        xml_space: parse_xml_space(attribute_value(&node, NS_XML, "space")),
1770        begin: node.attribute("begin").map(|s| s.to_string()),
1771        dur: node.attribute("dur").map(|s| s.to_string()),
1772        end: node.attribute("end").map(|s| s.to_string()),
1773        time_container: node.attribute("timeContainer").map(|s| s.to_string()),
1774        style: node.attribute("style").map(|s| s.to_string()),
1775        animate: node.attribute("animate").map(|s| s.to_string()),
1776        condition: node.attribute("condition").map(|s| s.to_string()),
1777        ttm_role: attribute_value(&node, NS_TTM, "role").map(|s| s.to_string()),
1778        style_attributes: style_attrs,
1779        other_attributes: other_attributes(&node),
1780    })
1781}
1782
1783fn parse_xml_space(value: Option<&str>) -> Option<XmlSpace> {
1784    match value {
1785        Some("preserve") => Some(XmlSpace::Preserve),
1786        Some("default") => Some(XmlSpace::Default),
1787        _ => None,
1788    }
1789}
1790
1791// ─── XML Serialization Functions ──────────────────────────────────
1792
1793/// Serialize the root `<tt>` element to an XML string buffer.
1794fn serialize_tt_element(tt: &TtElement, buf: &mut String, indent: usize) {
1795    let ind = "  ".repeat(indent);
1796    buf.push_str(&ind);
1797    buf.push_str("<tt");
1798    // Always output the default namespace and core namespaces
1799    buf.push_str(r#" xmlns="http://www.w3.org/ns/ttml""#);
1800    buf.push_str(r#" xmlns:tt="http://www.w3.org/ns/ttml""#);
1801    buf.push_str(r#" xmlns:ttp="http://www.w3.org/ns/ttml#parameter""#);
1802    buf.push_str(r#" xmlns:tts="http://www.w3.org/ns/ttml#styling""#);
1803    buf.push_str(r#" xmlns:ttm="http://www.w3.org/ns/ttml#metadata""#);
1804
1805    // Only add extension namespace bindings if they're actually used
1806    if tt_ns_needed(tt, NS_ITTS) {
1807        buf.push_str(r#" xmlns:itts="http://www.w3.org/ns/ttml/profile/imsc1#styling""#);
1808    }
1809    if tt_ns_needed(tt, NS_ITTP) {
1810        buf.push_str(r#" xmlns:ittp="http://www.w3.org/ns/ttml/profile/imsc1#parameter""#);
1811    }
1812    if tt_ns_needed(tt, NS_ITTM) {
1813        buf.push_str(r#" xmlns:ittm="http://www.w3.org/ns/ttml/profile/imsc1#metadata""#);
1814    }
1815    if tt_ns_needed(tt, NS_EBUTTM) {
1816        buf.push_str(r#" xmlns:ebuttm="urn:ebu:tt:metadata""#);
1817    }
1818    if tt_ns_needed(tt, NS_EBUTTS) {
1819        buf.push_str(r#" xmlns:ebutts="urn:ebu:tt:style""#);
1820    }
1821    if tt_ns_needed(tt, NS_SMPTE) {
1822        buf.push_str(r#" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt""#);
1823    }
1824    if tt_ns_needed(tt, NS_TTA) {
1825        buf.push_str(r#" xmlns:tta="http://www.w3.org/ns/ttml#audio""#);
1826    }
1827
1828    // xml:lang
1829    if let Some(ref lang) = tt.xml_lang {
1830        buf.push_str(&format!(r#" xml:lang="{}""#, xml_escape(lang)));
1831    }
1832
1833    // ttp attributes
1834    serialize_opt_attr(buf, "ttp:timeBase", &tt.ttp_time_base);
1835    serialize_opt_attr(buf, "ttp:frameRate", &tt.ttp_frame_rate);
1836    serialize_opt_attr(
1837        buf,
1838        "ttp:frameRateMultiplier",
1839        &tt.ttp_frame_rate_multiplier,
1840    );
1841    serialize_opt_attr(buf, "ttp:tickRate", &tt.ttp_tick_rate);
1842    serialize_opt_attr(buf, "ttp:subFrameRate", &tt.ttp_sub_frame_rate);
1843    serialize_opt_attr(buf, "ttp:dropMode", &tt.ttp_drop_mode);
1844    serialize_opt_attr(buf, "ttp:markerMode", &tt.ttp_marker_mode);
1845    serialize_opt_attr(buf, "ttp:clockMode", &tt.ttp_clock_mode);
1846    serialize_opt_attr(buf, "ttp:cellResolution", &tt.ttp_cell_resolution);
1847    serialize_opt_attr(buf, "ttp:pixelAspectRatio", &tt.ttp_pixel_aspect_ratio);
1848    serialize_opt_attr(buf, "ttp:displayAspectRatio", &tt.ttp_display_aspect_ratio);
1849    serialize_opt_attr(buf, "ttp:profile", &tt.ttp_profile);
1850    serialize_opt_attr(buf, "ttp:contentProfiles", &tt.ttp_content_profiles);
1851    serialize_opt_attr(
1852        buf,
1853        "ttp:contentProfileCombination",
1854        &tt.ttp_content_profile_combination,
1855    );
1856    serialize_opt_attr(buf, "ttp:processorProfiles", &tt.ttp_processor_profiles);
1857    serialize_opt_attr(
1858        buf,
1859        "ttp:processorProfileCombination",
1860        &tt.ttp_processor_profile_combination,
1861    );
1862    serialize_opt_attr(
1863        buf,
1864        "ttp:permitFeatureNarrowing",
1865        &tt.ttp_permit_feature_narrowing,
1866    );
1867    serialize_opt_attr(
1868        buf,
1869        "ttp:permitFeatureWidening",
1870        &tt.ttp_permit_feature_widening,
1871    );
1872    serialize_opt_attr(buf, "ttp:validation", &tt.ttp_validation);
1873    serialize_opt_attr(buf, "ttp:validationAction", &tt.ttp_validation_action);
1874
1875    // tts:extent on root
1876    serialize_opt_attr(buf, "tts:extent", &tt.tts_extent);
1877
1878    // IMSC extension attributes
1879    serialize_opt_attr(buf, "ittp:activeArea", &tt.ittp_active_area);
1880    serialize_opt_attr(buf, "ittp:aspectRatio", &tt.ittp_aspect_ratio);
1881    serialize_opt_attr(
1882        buf,
1883        "ittp:progressivelyDecodable",
1884        &tt.ittp_progressively_decodable,
1885    );
1886
1887    // xml:id
1888    serialize_opt_attr(buf, "xml:id", &tt.xml_id);
1889
1890    buf.push_str(">\n");
1891
1892    // head
1893    if let Some(ref head) = tt.head {
1894        serialize_head_element(head, buf, indent + 1);
1895    }
1896
1897    // body
1898    if let Some(ref body) = tt.body {
1899        serialize_body_element(body, buf, indent + 1);
1900    }
1901
1902    buf.push_str(&format!("{}</tt>\n", ind));
1903}
1904
1905fn tt_ns_needed(tt: &TtElement, ns: &str) -> bool {
1906    // Check tt-level attributes
1907    for (attr_ns, _) in tt.other_attributes.keys() {
1908        if attr_ns == ns {
1909            return true;
1910        }
1911    }
1912    // Check explicit tt-level attributes
1913    if ns == NS_ITTP
1914        && (tt.ittp_active_area.is_some()
1915            || tt.ittp_aspect_ratio.is_some()
1916            || tt.ittp_progressively_decodable.is_some())
1917    {
1918        return true;
1919    }
1920    // Check children for namespace usage
1921    let body_needed = if let Some(ref body) = tt.body {
1922        body_ns_needed(body, ns)
1923    } else {
1924        false
1925    };
1926    let head_needed = if let Some(ref head) = tt.head {
1927        head_ns_needed(head, ns)
1928    } else {
1929        false
1930    };
1931    body_needed || head_needed
1932}
1933
1934fn body_ns_needed(body: &BodyElement, ns: &str) -> bool {
1935    for (an, _) in body.other_attributes.keys() {
1936        if an == ns {
1937            return true;
1938        }
1939    }
1940    if style_ns_needed(&body.style_attributes, ns) {
1941        return true;
1942    }
1943    for div in &body.divs {
1944        if div_ns_needed(div, ns) {
1945            return true;
1946        }
1947    }
1948    for meta in &body.metadata {
1949        if meta_ns_needed(meta, ns) {
1950            return true;
1951        }
1952    }
1953    false
1954}
1955
1956fn head_ns_needed(head: &HeadElement, ns: &str) -> bool {
1957    for meta in &head.metadata {
1958        if meta_ns_needed(meta, ns) {
1959            return true;
1960        }
1961    }
1962    false
1963}
1964
1965fn div_ns_needed(div: &DivElement, ns: &str) -> bool {
1966    if div.smpte_background_image.is_some() && ns == NS_SMPTE {
1967        return true;
1968    }
1969    for (an, _) in div.other_attributes.keys() {
1970        if an == ns {
1971            return true;
1972        }
1973    }
1974    if style_ns_needed(&div.style_attributes, ns) {
1975        return true;
1976    }
1977    for p in &div.paragraphs {
1978        if style_ns_needed(&p.style_attributes, ns) {
1979            return true;
1980        }
1981        for (an, _) in p.other_attributes.keys() {
1982            if an == ns {
1983                return true;
1984            }
1985        }
1986        for item in &p.content {
1987            if inline_ns_needed(item, ns) {
1988                return true;
1989            }
1990        }
1991    }
1992    for meta in &div.metadata {
1993        if meta_ns_needed(meta, ns) {
1994            return true;
1995        }
1996    }
1997    for img in &div.images {
1998        if img.tts_extent.is_some() && ns == NS_TTS {
1999            return true;
2000        }
2001        if style_ns_needed(&img.style_attributes, ns) {
2002            return true;
2003        }
2004        for meta in &img.metadata {
2005            if meta_ns_needed(meta, ns) {
2006                return true;
2007            }
2008        }
2009    }
2010    false
2011}
2012
2013fn inline_ns_needed(item: &InlineContent, ns: &str) -> bool {
2014    match item {
2015        InlineContent::Text(_) => false,
2016        InlineContent::Span(span) => {
2017            if style_ns_needed(&span.style_attributes, ns) {
2018                return true;
2019            }
2020            for (an, _) in span.other_attributes.keys() {
2021                if an == ns {
2022                    return true;
2023                }
2024            }
2025            for child in &span.content {
2026                if inline_ns_needed(child, ns) {
2027                    return true;
2028                }
2029            }
2030            false
2031        }
2032        InlineContent::Br(_) => false,
2033    }
2034}
2035
2036fn meta_ns_needed(meta: &MetadataChild, ns: &str) -> bool {
2037    match meta {
2038        MetadataChild::Metadata(m) => {
2039            for c in &m.children {
2040                if meta_ns_needed(c, ns) {
2041                    return true;
2042                }
2043            }
2044            false
2045        }
2046        MetadataChild::EbuttmDocumentMetadata(eb) => {
2047            if ns == NS_EBUTTM {
2048                return true;
2049            }
2050            for c in &eb.children {
2051                if meta_ns_needed(c, ns) {
2052                    return true;
2053                }
2054            }
2055            false
2056        }
2057        MetadataChild::EbuttmConformsToStandard(_) => ns == NS_EBUTTM,
2058        MetadataChild::IttmAltText(_) => ns == NS_ITTM,
2059        _ => false,
2060    }
2061}
2062
2063fn style_ns_needed(attrs: &StyleAttributes, ns: &str) -> bool {
2064    match ns {
2065        NS_TTS => {
2066            attrs.tts_background_color.is_some()
2067                || attrs.tts_color.is_some()
2068                || attrs.tts_extent.is_some()
2069                || attrs.tts_origin.is_some()
2070                || attrs.tts_font_size.is_some()
2071                || attrs.tts_font_family.is_some()
2072                || attrs.tts_font_style.is_some()
2073                || attrs.tts_font_weight.is_some()
2074                || attrs.tts_display_align.is_some()
2075                || attrs.tts_text_align.is_some()
2076                || attrs.tts_text_emphasis.is_some()
2077                || attrs.tts_text_shadow.is_some()
2078                || attrs.tts_ruby.is_some()
2079                || attrs.tts_ruby_align.is_some()
2080                || attrs.tts_ruby_position.is_some()
2081                || attrs.tts_ruby_reserve.is_some()
2082                || attrs.tts_line_height.is_some()
2083                || attrs.tts_opacity.is_some()
2084                || attrs.tts_position.is_some()
2085                || attrs.tts_visibility.is_some()
2086                || attrs.tts_display.is_some()
2087                || attrs.tts_writing_mode.is_some()
2088                || attrs.tts_show_background.is_some()
2089                || attrs.tts_overflow.is_some()
2090                || attrs.tts_z_index.is_some()
2091                || attrs.tts_padding.is_some()
2092                || attrs.tts_luminance_gain.is_some()
2093                || attrs.tts_direction.is_some()
2094                || attrs.tts_unicode_bidi.is_some()
2095                || attrs.tts_wrap_option.is_some()
2096                || attrs.tts_text_combine.is_some()
2097                || attrs.tts_text_decoration.is_some()
2098                || attrs.tts_text_orientation.is_some()
2099                || attrs.tts_text_outline.is_some()
2100                || attrs.tts_shear.is_some()
2101                || attrs.tts_background_clip.is_some()
2102                || attrs.tts_background_extent.is_some()
2103                || attrs.tts_background_image.is_some()
2104                || attrs.tts_background_origin.is_some()
2105                || attrs.tts_background_position.is_some()
2106                || attrs.tts_background_repeat.is_some()
2107                || attrs.tts_border.is_some()
2108                || attrs.tts_bpd.is_some()
2109                || attrs.tts_disparity.is_some()
2110                || attrs.tts_font_kerning.is_some()
2111                || attrs.tts_font_selection_strategy.is_some()
2112                || attrs.tts_font_shear.is_some()
2113                || attrs.tts_font_variant.is_some()
2114                || attrs.tts_ipd.is_some()
2115                || attrs.tts_letter_spacing.is_some()
2116                || attrs.tts_line_shear.is_some()
2117        }
2118        NS_ITTS => attrs.itts_forced_display.is_some() || attrs.itts_fill_line_gap.is_some(),
2119        NS_EBUTTS => attrs.ebutts_line_padding.is_some() || attrs.ebutts_multi_row_align.is_some(),
2120        NS_TTA => {
2121            attrs.tta_gain.is_some()
2122                || attrs.tta_pan.is_some()
2123                || attrs.tta_pitch.is_some()
2124                || attrs.tta_speak.is_some()
2125        }
2126        _ => false,
2127    }
2128}
2129
2130fn serialize_opt_attr(buf: &mut String, name: &str, value: &Option<String>) {
2131    if let Some(v) = value
2132        && !v.is_empty()
2133    {
2134        buf.push_str(&format!(r#" {}="{}""#, name, xml_escape(v)));
2135    }
2136}
2137
2138fn serialize_head_element(head: &HeadElement, buf: &mut String, indent: usize) {
2139    let ind = "  ".repeat(indent);
2140    buf.push_str(&format!("{}<head>\n", ind));
2141
2142    // Metadata (ttm:title, ttm:desc, etc.)
2143    for meta in &head.metadata {
2144        serialize_metadata_child(meta, buf, indent + 1);
2145    }
2146
2147    // Styling
2148    if let Some(ref styling) = head.styling {
2149        serialize_styling_element(styling, buf, indent + 1);
2150    }
2151
2152    // Layout
2153    if let Some(ref layout) = head.layout {
2154        serialize_layout_element(layout, buf, indent + 1);
2155    }
2156
2157    buf.push_str(&format!("{}</head>\n", ind));
2158}
2159
2160fn serialize_body_element(body: &BodyElement, buf: &mut String, indent: usize) {
2161    let ind = "  ".repeat(indent);
2162    buf.push_str(&format!("{}<body", ind));
2163
2164    serialize_common_timing_attrs(
2165        buf,
2166        body.begin.as_deref(),
2167        body.dur.as_deref(),
2168        body.end.as_deref(),
2169        body.time_container.as_deref(),
2170    );
2171    serialize_opt_attr(buf, "region", &body.region);
2172    serialize_opt_attr(buf, "style", &body.style);
2173    serialize_opt_attr(buf, "animate", &body.animate);
2174    serialize_opt_attr(buf, "condition", &body.condition);
2175    serialize_style_attrs(&body.style_attributes, buf);
2176    serialize_opt_attr(buf, "xml:id", &body.xml_id);
2177    serialize_opt_attr(buf, "xml:lang", &body.xml_lang);
2178
2179    if body.divs.is_empty() && body.metadata.is_empty() && body.animations.is_empty() {
2180        buf.push_str("/>\n");
2181    } else {
2182        buf.push_str(">\n");
2183
2184        for meta in &body.metadata {
2185            serialize_metadata_child(meta, buf, indent + 1);
2186        }
2187        for anim in &body.animations {
2188            serialize_animation_child(anim, buf, indent + 1);
2189        }
2190        for div in &body.divs {
2191            serialize_div_element(div, buf, indent + 1);
2192        }
2193
2194        buf.push_str(&format!("{}</body>\n", ind));
2195    }
2196}
2197
2198fn serialize_div_element(div: &DivElement, buf: &mut String, indent: usize) {
2199    let ind = "  ".repeat(indent);
2200    buf.push_str(&format!("{}<div", ind));
2201
2202    serialize_common_timing_attrs(
2203        buf,
2204        div.begin.as_deref(),
2205        div.dur.as_deref(),
2206        div.end.as_deref(),
2207        div.time_container.as_deref(),
2208    );
2209    serialize_opt_attr(buf, "region", &div.region);
2210    serialize_opt_attr(buf, "style", &div.style);
2211    serialize_opt_attr(buf, "animate", &div.animate);
2212    serialize_opt_attr(buf, "condition", &div.condition);
2213    serialize_style_attrs(&div.style_attributes, buf);
2214    serialize_opt_attr(buf, "smpte:backgroundImage", &div.smpte_background_image);
2215    serialize_opt_attr(buf, "xml:id", &div.xml_id);
2216    serialize_opt_attr(buf, "xml:lang", &div.xml_lang);
2217
2218    let has_children = !div.paragraphs.is_empty()
2219        || !div.images.is_empty()
2220        || !div.metadata.is_empty()
2221        || !div.animations.is_empty();
2222    if !has_children {
2223        buf.push_str("/>\n");
2224    } else {
2225        buf.push_str(">\n");
2226
2227        for meta in &div.metadata {
2228            serialize_metadata_child(meta, buf, indent + 1);
2229        }
2230        for anim in &div.animations {
2231            serialize_animation_child(anim, buf, indent + 1);
2232        }
2233        for p in &div.paragraphs {
2234            serialize_p_element(p, buf, indent + 1);
2235        }
2236        for img in &div.images {
2237            serialize_image_element(img, buf, indent + 1);
2238        }
2239
2240        buf.push_str(&format!("{}</div>\n", ind));
2241    }
2242}
2243
2244fn serialize_p_element(p: &PElement, buf: &mut String, indent: usize) {
2245    let ind = "  ".repeat(indent);
2246    buf.push_str(&format!("{}<p", ind));
2247
2248    serialize_common_timing_attrs(
2249        buf,
2250        p.begin.as_deref(),
2251        p.dur.as_deref(),
2252        p.end.as_deref(),
2253        p.time_container.as_deref(),
2254    );
2255    serialize_opt_attr(buf, "region", &p.region);
2256    serialize_opt_attr(buf, "style", &p.style);
2257    serialize_opt_attr(buf, "animate", &p.animate);
2258    serialize_opt_attr(buf, "condition", &p.condition);
2259    serialize_style_attrs(&p.style_attributes, buf);
2260    serialize_opt_attr(buf, "xml:id", &p.xml_id);
2261    serialize_opt_attr(buf, "xml:lang", &p.xml_lang);
2262
2263    let has_children = !p.content.is_empty() || !p.metadata.is_empty() || !p.animations.is_empty();
2264    if !has_children {
2265        buf.push_str("/>\n");
2266    } else {
2267        buf.push('>');
2268        for meta in &p.metadata {
2269            buf.push('\n');
2270            serialize_metadata_child(meta, buf, indent + 1);
2271        }
2272        for anim in &p.animations {
2273            buf.push('\n');
2274            serialize_animation_child(anim, buf, indent + 1);
2275        }
2276        for item in &p.content {
2277            serialize_inline_content(item, buf);
2278        }
2279        buf.push_str("</p>\n");
2280    }
2281}
2282
2283fn serialize_inline_content(content: &InlineContent, buf: &mut String) {
2284    match content {
2285        InlineContent::Text(text) => {
2286            buf.push_str(&xml_escape(text));
2287        }
2288        InlineContent::Span(span) => {
2289            buf.push_str("<span");
2290            serialize_common_timing_attrs(
2291                buf,
2292                span.begin.as_deref(),
2293                span.dur.as_deref(),
2294                span.end.as_deref(),
2295                span.time_container.as_deref(),
2296            );
2297            serialize_opt_attr(buf, "region", &span.region);
2298            serialize_opt_attr(buf, "style", &span.style);
2299            serialize_opt_attr(buf, "animate", &span.animate);
2300            serialize_style_attrs(&span.style_attributes, buf);
2301            serialize_opt_attr(buf, "xml:id", &span.xml_id);
2302            serialize_opt_attr(buf, "xml:lang", &span.xml_lang);
2303
2304            if span.content.is_empty() {
2305                buf.push_str("/>");
2306            } else {
2307                buf.push('>');
2308                for item in &span.content {
2309                    serialize_inline_content(item, buf);
2310                }
2311                buf.push_str("</span>");
2312            }
2313        }
2314        InlineContent::Br(_br) => {
2315            buf.push_str("<br/>");
2316        }
2317    }
2318}
2319
2320fn serialize_image_element(image: &ImageElement, buf: &mut String, indent: usize) {
2321    let ind = "  ".repeat(indent);
2322    buf.push_str(&format!("{}<image", ind));
2323
2324    serialize_common_timing_attrs(
2325        buf,
2326        image.begin.as_deref(),
2327        image.dur.as_deref(),
2328        image.end.as_deref(),
2329        image.time_container.as_deref(),
2330    );
2331    serialize_opt_attr(buf, "region", &image.region);
2332    serialize_opt_attr(buf, "style", &image.style);
2333    serialize_opt_attr(buf, "animate", &image.animate);
2334    serialize_opt_attr(buf, "condition", &image.condition);
2335    serialize_opt_attr(buf, "src", &image.src);
2336    serialize_opt_attr(buf, "type", &image.type_);
2337    // tts:extent is explicitly on ImageElement — use that, not style_attributes
2338    serialize_opt_attr(buf, "tts:extent", &image.tts_extent);
2339    // Serialize remaining style attributes but skip tts:extent (already handled)
2340    serialize_style_attrs_skip_extent(&image.style_attributes, buf);
2341    serialize_opt_attr(buf, "xml:id", &image.xml_id);
2342    serialize_opt_attr(buf, "xml:lang", &image.xml_lang);
2343
2344    if image.metadata.is_empty() {
2345        buf.push_str("/>\n");
2346    } else {
2347        buf.push_str(">\n");
2348        for meta in &image.metadata {
2349            serialize_metadata_child(meta, buf, indent + 1);
2350        }
2351        buf.push_str(&format!("{}</image>\n", ind));
2352    }
2353}
2354
2355fn serialize_metadata_child(child: &MetadataChild, buf: &mut String, indent: usize) {
2356    let ind = "  ".repeat(indent);
2357    match child {
2358        MetadataChild::Metadata(m) => {
2359            buf.push_str(&format!("{}<metadata>\n", ind));
2360            for c in &m.children {
2361                serialize_metadata_child(c, buf, indent + 1);
2362            }
2363            buf.push_str(&format!("{}</metadata>\n", ind));
2364        }
2365        MetadataChild::TtmTitle(t) => {
2366            buf.push_str(&format!(
2367                "{}<ttm:title>{}</ttm:title>\n",
2368                ind,
2369                xml_escape(&t.text)
2370            ));
2371        }
2372        MetadataChild::TtmDesc(t) => {
2373            buf.push_str(&format!(
2374                "{}<ttm:desc>{}</ttm:desc>\n",
2375                ind,
2376                xml_escape(&t.text)
2377            ));
2378        }
2379        MetadataChild::TtmCopyright(t) => {
2380            buf.push_str(&format!(
2381                "{}<ttm:copyright>{}</ttm:copyright>\n",
2382                ind,
2383                xml_escape(&t.text)
2384            ));
2385        }
2386        MetadataChild::TtmAgent(a) => {
2387            buf.push_str(&format!("{}<ttm:agent", ind));
2388            serialize_opt_attr(buf, "type", &a.type_);
2389            buf.push_str(">\n");
2390            for name in &a.names {
2391                buf.push_str(&format!(
2392                    "{}<ttm:name>{}</ttm:name>\n",
2393                    "  ".repeat(indent + 1),
2394                    xml_escape(&name.text)
2395                ));
2396            }
2397            buf.push_str(&format!("{}</ttm:agent>\n", ind));
2398        }
2399        MetadataChild::TtmItem(item) => {
2400            serialize_ttm_item(item, buf, indent);
2401        }
2402        MetadataChild::TtmName(n) => {
2403            buf.push_str(&format!(
2404                "{}<ttm:name>{}</ttm:name>\n",
2405                ind,
2406                xml_escape(&n.text)
2407            ));
2408        }
2409        MetadataChild::EbuttmDocumentMetadata(eb) => {
2410            buf.push_str(&format!("{}<ebuttm:documentMetadata>\n", ind));
2411            for c in &eb.children {
2412                serialize_metadata_child(c, buf, indent + 1);
2413            }
2414            buf.push_str(&format!("{}</ebuttm:documentMetadata>\n", ind));
2415        }
2416        MetadataChild::EbuttmConformsToStandard(cs) => {
2417            buf.push_str(&format!(
2418                "{}<ebuttm:conformsToStandard>{}</ebuttm:conformsToStandard>\n",
2419                ind,
2420                xml_escape(&cs.text)
2421            ));
2422        }
2423        MetadataChild::IttmAltText(alt) => {
2424            buf.push_str(&format!(
2425                "{}<ittm:altText>{}</ittm:altText>\n",
2426                ind,
2427                xml_escape(&alt.text)
2428            ));
2429        }
2430    }
2431}
2432
2433fn serialize_ttm_item(item: &TtmItemElement, buf: &mut String, indent: usize) {
2434    let ind = "  ".repeat(indent);
2435    buf.push_str(&format!("{}<ttm:item", ind));
2436    serialize_opt_attr(buf, "name", &item.name);
2437    if item.items.is_empty() && item.text.is_none() {
2438        buf.push_str("/>\n");
2439    } else if item.items.is_empty() {
2440        buf.push('>');
2441        if let Some(ref t) = item.text {
2442            buf.push_str(&xml_escape(t));
2443        }
2444        buf.push_str("</ttm:item>\n");
2445    } else {
2446        buf.push_str(">\n");
2447        for i in &item.items {
2448            serialize_ttm_item(i, buf, indent + 1);
2449        }
2450        buf.push_str(&format!("{}</ttm:item>\n", ind));
2451    }
2452}
2453
2454fn serialize_animation_child(anim: &AnimationChild, buf: &mut String, indent: usize) {
2455    let ind = "  ".repeat(indent);
2456    match anim {
2457        AnimationChild::Set(set) => {
2458            buf.push_str(&format!("{}<set", ind));
2459            serialize_common_timing_attrs(
2460                buf,
2461                set.begin.as_deref(),
2462                set.dur.as_deref(),
2463                set.end.as_deref(),
2464                None,
2465            );
2466            serialize_opt_attr(buf, "fill", &set.fill);
2467            serialize_opt_attr(buf, "repeatCount", &set.repeat_count);
2468            serialize_style_attrs(&set.style_attributes, buf);
2469            buf.push_str("/>\n");
2470        }
2471    }
2472}
2473
2474fn serialize_styling_element(styling: &StylingElement, buf: &mut String, indent: usize) {
2475    let ind = "  ".repeat(indent);
2476    buf.push_str(&format!("{}<styling>\n", ind));
2477    for init in &styling.initials {
2478        buf.push_str(&format!("{}<initial", "  ".repeat(indent + 1)));
2479        serialize_style_attrs(&init.style_attributes, buf);
2480        buf.push_str("/>\n");
2481    }
2482    for style in &styling.styles {
2483        buf.push_str(&format!("{}<style", "  ".repeat(indent + 1)));
2484        serialize_opt_attr(buf, "xml:id", &style.xml_id);
2485        serialize_opt_attr(buf, "style", &style.style);
2486        serialize_style_attrs(&style.style_attributes, buf);
2487        buf.push_str("/>\n");
2488    }
2489    buf.push_str(&format!("{}</styling>\n", ind));
2490}
2491
2492fn serialize_layout_element(layout: &LayoutElement, buf: &mut String, indent: usize) {
2493    let ind = "  ".repeat(indent);
2494    buf.push_str(&format!("{}<layout>\n", ind));
2495    for region in &layout.regions {
2496        serialize_region_element(region, buf, indent + 1);
2497    }
2498    buf.push_str(&format!("{}</layout>\n", ind));
2499}
2500
2501fn serialize_region_element(region: &RegionElement, buf: &mut String, indent: usize) {
2502    let ind = "  ".repeat(indent);
2503    buf.push_str(&format!("{}<region", ind));
2504
2505    serialize_common_timing_attrs(
2506        buf,
2507        region.begin.as_deref(),
2508        region.dur.as_deref(),
2509        region.end.as_deref(),
2510        region.time_container.as_deref(),
2511    );
2512    serialize_opt_attr(buf, "style", &region.style);
2513    serialize_opt_attr(buf, "animate", &region.animate);
2514    serialize_opt_attr(buf, "condition", &region.condition);
2515    serialize_opt_attr(buf, "ttm:role", &region.ttm_role);
2516    serialize_style_attrs(&region.style_attributes, buf);
2517    serialize_opt_attr(buf, "xml:id", &region.xml_id);
2518    serialize_opt_attr(buf, "xml:lang", &region.xml_lang);
2519
2520    buf.push_str("/>\n");
2521}
2522
2523fn serialize_common_timing_attrs(
2524    buf: &mut String,
2525    begin: Option<&str>,
2526    dur: Option<&str>,
2527    end: Option<&str>,
2528    time_container: Option<&str>,
2529) {
2530    if let Some(b) = begin {
2531        buf.push_str(&format!(r#" begin="{}""#, xml_escape(b)));
2532    }
2533    if let Some(d) = dur {
2534        buf.push_str(&format!(r#" dur="{}""#, xml_escape(d)));
2535    }
2536    if let Some(e) = end {
2537        buf.push_str(&format!(r#" end="{}""#, xml_escape(e)));
2538    }
2539    if let Some(tc) = time_container {
2540        buf.push_str(&format!(r#" timeContainer="{}""#, xml_escape(tc)));
2541    }
2542}
2543
2544/// Like `serialize_style_attrs` but skips the tts:extent attribute
2545/// (used when tts:extent is already output via an explicit field like on ImageElement).
2546fn serialize_style_attrs_skip_extent(attrs: &StyleAttributes, buf: &mut String) {
2547    serialize_opt_attr(buf, "tts:backgroundColor", &attrs.tts_background_color);
2548    serialize_opt_attr(buf, "tts:backgroundClip", &attrs.tts_background_clip);
2549    serialize_opt_attr(buf, "tts:backgroundExtent", &attrs.tts_background_extent);
2550    serialize_opt_attr(buf, "tts:backgroundImage", &attrs.tts_background_image);
2551    serialize_opt_attr(buf, "tts:backgroundOrigin", &attrs.tts_background_origin);
2552    serialize_opt_attr(
2553        buf,
2554        "tts:backgroundPosition",
2555        &attrs.tts_background_position,
2556    );
2557    serialize_opt_attr(buf, "tts:backgroundRepeat", &attrs.tts_background_repeat);
2558    serialize_opt_attr(buf, "tts:border", &attrs.tts_border);
2559    serialize_opt_attr(buf, "tts:bpd", &attrs.tts_bpd);
2560    serialize_opt_attr(buf, "tts:color", &attrs.tts_color);
2561    serialize_opt_attr(buf, "tts:direction", &attrs.tts_direction);
2562    serialize_opt_attr(buf, "tts:disparity", &attrs.tts_disparity);
2563    serialize_opt_attr(buf, "tts:display", &attrs.tts_display);
2564    serialize_opt_attr(buf, "tts:displayAlign", &attrs.tts_display_align);
2565    serialize_opt_attr(buf, "tts:fontFamily", &attrs.tts_font_family);
2566    serialize_opt_attr(buf, "tts:fontKerning", &attrs.tts_font_kerning);
2567    serialize_opt_attr(
2568        buf,
2569        "tts:fontSelectionStrategy",
2570        &attrs.tts_font_selection_strategy,
2571    );
2572    serialize_opt_attr(buf, "tts:fontShear", &attrs.tts_font_shear);
2573    serialize_opt_attr(buf, "tts:fontSize", &attrs.tts_font_size);
2574    serialize_opt_attr(buf, "tts:fontStyle", &attrs.tts_font_style);
2575    serialize_opt_attr(buf, "tts:fontVariant", &attrs.tts_font_variant);
2576    serialize_opt_attr(buf, "tts:fontWeight", &attrs.tts_font_weight);
2577    serialize_opt_attr(buf, "tts:ipd", &attrs.tts_ipd);
2578    serialize_opt_attr(buf, "tts:letterSpacing", &attrs.tts_letter_spacing);
2579    serialize_opt_attr(buf, "tts:lineHeight", &attrs.tts_line_height);
2580    serialize_opt_attr(buf, "tts:lineShear", &attrs.tts_line_shear);
2581    serialize_opt_attr(buf, "tts:luminanceGain", &attrs.tts_luminance_gain);
2582    serialize_opt_attr(buf, "tts:opacity", &attrs.tts_opacity);
2583    serialize_opt_attr(buf, "tts:origin", &attrs.tts_origin);
2584    serialize_opt_attr(buf, "tts:overflow", &attrs.tts_overflow);
2585    serialize_opt_attr(buf, "tts:padding", &attrs.tts_padding);
2586    serialize_opt_attr(buf, "tts:position", &attrs.tts_position);
2587    serialize_opt_attr(buf, "tts:ruby", &attrs.tts_ruby);
2588    serialize_opt_attr(buf, "tts:rubyAlign", &attrs.tts_ruby_align);
2589    serialize_opt_attr(buf, "tts:rubyPosition", &attrs.tts_ruby_position);
2590    serialize_opt_attr(buf, "tts:rubyReserve", &attrs.tts_ruby_reserve);
2591    serialize_opt_attr(buf, "tts:shear", &attrs.tts_shear);
2592    serialize_opt_attr(buf, "tts:showBackground", &attrs.tts_show_background);
2593    serialize_opt_attr(buf, "tts:textAlign", &attrs.tts_text_align);
2594    serialize_opt_attr(buf, "tts:textCombine", &attrs.tts_text_combine);
2595    serialize_opt_attr(buf, "tts:textDecoration", &attrs.tts_text_decoration);
2596    serialize_opt_attr(buf, "tts:textEmphasis", &attrs.tts_text_emphasis);
2597    serialize_opt_attr(buf, "tts:textOrientation", &attrs.tts_text_orientation);
2598    serialize_opt_attr(buf, "tts:textOutline", &attrs.tts_text_outline);
2599    serialize_opt_attr(buf, "tts:textShadow", &attrs.tts_text_shadow);
2600    serialize_opt_attr(buf, "tts:unicodeBidi", &attrs.tts_unicode_bidi);
2601    serialize_opt_attr(buf, "tts:visibility", &attrs.tts_visibility);
2602    serialize_opt_attr(buf, "tts:wrapOption", &attrs.tts_wrap_option);
2603    serialize_opt_attr(buf, "tts:writingMode", &attrs.tts_writing_mode);
2604    serialize_opt_attr(buf, "tts:zIndex", &attrs.tts_z_index);
2605    serialize_opt_attr(buf, "tta:gain", &attrs.tta_gain);
2606    serialize_opt_attr(buf, "tta:pan", &attrs.tta_pan);
2607    serialize_opt_attr(buf, "tta:pitch", &attrs.tta_pitch);
2608    serialize_opt_attr(buf, "tta:speak", &attrs.tta_speak);
2609    serialize_opt_attr(buf, "itts:forcedDisplay", &attrs.itts_forced_display);
2610    serialize_opt_attr(buf, "itts:fillLineGap", &attrs.itts_fill_line_gap);
2611    serialize_opt_attr(buf, "ebutts:linePadding", &attrs.ebutts_line_padding);
2612    serialize_opt_attr(buf, "ebutts:multiRowAlign", &attrs.ebutts_multi_row_align);
2613}
2614
2615#[allow(clippy::too_many_lines)]
2616fn serialize_style_attrs(attrs: &StyleAttributes, buf: &mut String) {
2617    serialize_opt_attr(buf, "tts:backgroundColor", &attrs.tts_background_color);
2618    serialize_opt_attr(buf, "tts:backgroundClip", &attrs.tts_background_clip);
2619    serialize_opt_attr(buf, "tts:backgroundExtent", &attrs.tts_background_extent);
2620    serialize_opt_attr(buf, "tts:backgroundImage", &attrs.tts_background_image);
2621    serialize_opt_attr(buf, "tts:backgroundOrigin", &attrs.tts_background_origin);
2622    serialize_opt_attr(
2623        buf,
2624        "tts:backgroundPosition",
2625        &attrs.tts_background_position,
2626    );
2627    serialize_opt_attr(buf, "tts:backgroundRepeat", &attrs.tts_background_repeat);
2628    serialize_opt_attr(buf, "tts:border", &attrs.tts_border);
2629    serialize_opt_attr(buf, "tts:bpd", &attrs.tts_bpd);
2630    serialize_opt_attr(buf, "tts:color", &attrs.tts_color);
2631    serialize_opt_attr(buf, "tts:direction", &attrs.tts_direction);
2632    serialize_opt_attr(buf, "tts:disparity", &attrs.tts_disparity);
2633    serialize_opt_attr(buf, "tts:display", &attrs.tts_display);
2634    serialize_opt_attr(buf, "tts:displayAlign", &attrs.tts_display_align);
2635    serialize_opt_attr(buf, "tts:extent", &attrs.tts_extent);
2636    serialize_opt_attr(buf, "tts:fontFamily", &attrs.tts_font_family);
2637    serialize_opt_attr(buf, "tts:fontKerning", &attrs.tts_font_kerning);
2638    serialize_opt_attr(
2639        buf,
2640        "tts:fontSelectionStrategy",
2641        &attrs.tts_font_selection_strategy,
2642    );
2643    serialize_opt_attr(buf, "tts:fontShear", &attrs.tts_font_shear);
2644    serialize_opt_attr(buf, "tts:fontSize", &attrs.tts_font_size);
2645    serialize_opt_attr(buf, "tts:fontStyle", &attrs.tts_font_style);
2646    serialize_opt_attr(buf, "tts:fontVariant", &attrs.tts_font_variant);
2647    serialize_opt_attr(buf, "tts:fontWeight", &attrs.tts_font_weight);
2648    serialize_opt_attr(buf, "tts:ipd", &attrs.tts_ipd);
2649    serialize_opt_attr(buf, "tts:letterSpacing", &attrs.tts_letter_spacing);
2650    serialize_opt_attr(buf, "tts:lineHeight", &attrs.tts_line_height);
2651    serialize_opt_attr(buf, "tts:lineShear", &attrs.tts_line_shear);
2652    serialize_opt_attr(buf, "tts:luminanceGain", &attrs.tts_luminance_gain);
2653    serialize_opt_attr(buf, "tts:opacity", &attrs.tts_opacity);
2654    serialize_opt_attr(buf, "tts:origin", &attrs.tts_origin);
2655    serialize_opt_attr(buf, "tts:overflow", &attrs.tts_overflow);
2656    serialize_opt_attr(buf, "tts:padding", &attrs.tts_padding);
2657    serialize_opt_attr(buf, "tts:position", &attrs.tts_position);
2658    serialize_opt_attr(buf, "tts:ruby", &attrs.tts_ruby);
2659    serialize_opt_attr(buf, "tts:rubyAlign", &attrs.tts_ruby_align);
2660    serialize_opt_attr(buf, "tts:rubyPosition", &attrs.tts_ruby_position);
2661    serialize_opt_attr(buf, "tts:rubyReserve", &attrs.tts_ruby_reserve);
2662    serialize_opt_attr(buf, "tts:shear", &attrs.tts_shear);
2663    serialize_opt_attr(buf, "tts:showBackground", &attrs.tts_show_background);
2664    serialize_opt_attr(buf, "tts:textAlign", &attrs.tts_text_align);
2665    serialize_opt_attr(buf, "tts:textCombine", &attrs.tts_text_combine);
2666    serialize_opt_attr(buf, "tts:textDecoration", &attrs.tts_text_decoration);
2667    serialize_opt_attr(buf, "tts:textEmphasis", &attrs.tts_text_emphasis);
2668    serialize_opt_attr(buf, "tts:textOrientation", &attrs.tts_text_orientation);
2669    serialize_opt_attr(buf, "tts:textOutline", &attrs.tts_text_outline);
2670    serialize_opt_attr(buf, "tts:textShadow", &attrs.tts_text_shadow);
2671    serialize_opt_attr(buf, "tts:unicodeBidi", &attrs.tts_unicode_bidi);
2672    serialize_opt_attr(buf, "tts:visibility", &attrs.tts_visibility);
2673    serialize_opt_attr(buf, "tts:wrapOption", &attrs.tts_wrap_option);
2674    serialize_opt_attr(buf, "tts:writingMode", &attrs.tts_writing_mode);
2675    serialize_opt_attr(buf, "tts:zIndex", &attrs.tts_z_index);
2676    serialize_opt_attr(buf, "tta:gain", &attrs.tta_gain);
2677    serialize_opt_attr(buf, "tta:pan", &attrs.tta_pan);
2678    serialize_opt_attr(buf, "tta:pitch", &attrs.tta_pitch);
2679    serialize_opt_attr(buf, "tta:speak", &attrs.tta_speak);
2680    serialize_opt_attr(buf, "itts:forcedDisplay", &attrs.itts_forced_display);
2681    serialize_opt_attr(buf, "itts:fillLineGap", &attrs.itts_fill_line_gap);
2682    serialize_opt_attr(buf, "ebutts:linePadding", &attrs.ebutts_line_padding);
2683    serialize_opt_attr(buf, "ebutts:multiRowAlign", &attrs.ebutts_multi_row_align);
2684}
2685
2686/// Basic XML escaping for attribute values and text content.
2687fn xml_escape(s: &str) -> String {
2688    s.replace('&', "&amp;")
2689        .replace('<', "&lt;")
2690        .replace('>', "&gt;")
2691        .replace('"', "&quot;")
2692        .replace('\'', "&apos;")
2693}
2694
2695// ─── Style attribute parsing ──────────────────────────────────────
2696
2697/// Parse all TT Style attributes (and IMSC/EBU style extensions) from an element node.
2698#[allow(clippy::too_many_lines)]
2699fn parse_style_attributes(node: roxmltree::Node<'_, '_>) -> StyleAttributes {
2700    StyleAttributes {
2701        tts_background_color: attribute_value(&node, NS_TTS, "backgroundColor")
2702            .map(|s| s.to_string()),
2703        tts_background_clip: attribute_value(&node, NS_TTS, "backgroundClip")
2704            .map(|s| s.to_string()),
2705        tts_background_extent: attribute_value(&node, NS_TTS, "backgroundExtent")
2706            .map(|s| s.to_string()),
2707        tts_background_image: attribute_value(&node, NS_TTS, "backgroundImage")
2708            .map(|s| s.to_string()),
2709        tts_background_origin: attribute_value(&node, NS_TTS, "backgroundOrigin")
2710            .map(|s| s.to_string()),
2711        tts_background_position: attribute_value(&node, NS_TTS, "backgroundPosition")
2712            .map(|s| s.to_string()),
2713        tts_background_repeat: attribute_value(&node, NS_TTS, "backgroundRepeat")
2714            .map(|s| s.to_string()),
2715        tts_border: attribute_value(&node, NS_TTS, "border").map(|s| s.to_string()),
2716        tts_bpd: attribute_value(&node, NS_TTS, "bpd").map(|s| s.to_string()),
2717        tts_color: attribute_value(&node, NS_TTS, "color").map(|s| s.to_string()),
2718        tts_direction: attribute_value(&node, NS_TTS, "direction").map(|s| s.to_string()),
2719        tts_disparity: attribute_value(&node, NS_TTS, "disparity").map(|s| s.to_string()),
2720        tts_display: attribute_value(&node, NS_TTS, "display").map(|s| s.to_string()),
2721        tts_display_align: attribute_value(&node, NS_TTS, "displayAlign").map(|s| s.to_string()),
2722        tts_extent: attribute_value(&node, NS_TTS, "extent").map(|s| s.to_string()),
2723        tts_font_family: attribute_value(&node, NS_TTS, "fontFamily").map(|s| s.to_string()),
2724        tts_font_kerning: attribute_value(&node, NS_TTS, "fontKerning").map(|s| s.to_string()),
2725        tts_font_selection_strategy: attribute_value(&node, NS_TTS, "fontSelectionStrategy")
2726            .map(|s| s.to_string()),
2727        tts_font_shear: attribute_value(&node, NS_TTS, "fontShear").map(|s| s.to_string()),
2728        tts_font_size: attribute_value(&node, NS_TTS, "fontSize").map(|s| s.to_string()),
2729        tts_font_style: attribute_value(&node, NS_TTS, "fontStyle").map(|s| s.to_string()),
2730        tts_font_variant: attribute_value(&node, NS_TTS, "fontVariant").map(|s| s.to_string()),
2731        tts_font_weight: attribute_value(&node, NS_TTS, "fontWeight").map(|s| s.to_string()),
2732        tts_ipd: attribute_value(&node, NS_TTS, "ipd").map(|s| s.to_string()),
2733        tts_letter_spacing: attribute_value(&node, NS_TTS, "letterSpacing").map(|s| s.to_string()),
2734        tts_line_height: attribute_value(&node, NS_TTS, "lineHeight").map(|s| s.to_string()),
2735        tts_line_shear: attribute_value(&node, NS_TTS, "lineShear").map(|s| s.to_string()),
2736        tts_luminance_gain: attribute_value(&node, NS_TTS, "luminanceGain").map(|s| s.to_string()),
2737        tts_opacity: attribute_value(&node, NS_TTS, "opacity").map(|s| s.to_string()),
2738        tts_origin: attribute_value(&node, NS_TTS, "origin").map(|s| s.to_string()),
2739        tts_overflow: attribute_value(&node, NS_TTS, "overflow").map(|s| s.to_string()),
2740        tts_padding: attribute_value(&node, NS_TTS, "padding").map(|s| s.to_string()),
2741        tts_position: attribute_value(&node, NS_TTS, "position").map(|s| s.to_string()),
2742        tts_ruby: attribute_value(&node, NS_TTS, "ruby").map(|s| s.to_string()),
2743        tts_ruby_align: attribute_value(&node, NS_TTS, "rubyAlign").map(|s| s.to_string()),
2744        tts_ruby_position: attribute_value(&node, NS_TTS, "rubyPosition").map(|s| s.to_string()),
2745        tts_ruby_reserve: attribute_value(&node, NS_TTS, "rubyReserve").map(|s| s.to_string()),
2746        tts_shear: attribute_value(&node, NS_TTS, "shear").map(|s| s.to_string()),
2747        tts_show_background: attribute_value(&node, NS_TTS, "showBackground")
2748            .map(|s| s.to_string()),
2749        tts_text_align: attribute_value(&node, NS_TTS, "textAlign").map(|s| s.to_string()),
2750        tts_text_combine: attribute_value(&node, NS_TTS, "textCombine").map(|s| s.to_string()),
2751        tts_text_decoration: attribute_value(&node, NS_TTS, "textDecoration")
2752            .map(|s| s.to_string()),
2753        tts_text_emphasis: attribute_value(&node, NS_TTS, "textEmphasis").map(|s| s.to_string()),
2754        tts_text_orientation: attribute_value(&node, NS_TTS, "textOrientation")
2755            .map(|s| s.to_string()),
2756        tts_text_outline: attribute_value(&node, NS_TTS, "textOutline").map(|s| s.to_string()),
2757        tts_text_shadow: attribute_value(&node, NS_TTS, "textShadow").map(|s| s.to_string()),
2758        tts_unicode_bidi: attribute_value(&node, NS_TTS, "unicodeBidi").map(|s| s.to_string()),
2759        tts_visibility: attribute_value(&node, NS_TTS, "visibility").map(|s| s.to_string()),
2760        tts_wrap_option: attribute_value(&node, NS_TTS, "wrapOption").map(|s| s.to_string()),
2761        tts_writing_mode: attribute_value(&node, NS_TTS, "writingMode").map(|s| s.to_string()),
2762        tts_z_index: attribute_value(&node, NS_TTS, "zIndex").map(|s| s.to_string()),
2763        tta_gain: attribute_value(&node, NS_TTA, "gain").map(|s| s.to_string()),
2764        tta_pan: attribute_value(&node, NS_TTA, "pan").map(|s| s.to_string()),
2765        tta_pitch: attribute_value(&node, NS_TTA, "pitch").map(|s| s.to_string()),
2766        tta_speak: attribute_value(&node, NS_TTA, "speak").map(|s| s.to_string()),
2767        itts_forced_display: attribute_value(&node, NS_ITTS, "forcedDisplay")
2768            .map(|s| s.to_string()),
2769        itts_fill_line_gap: attribute_value(&node, NS_ITTS, "fillLineGap").map(|s| s.to_string()),
2770        ebutts_line_padding: attribute_value(&node, NS_EBUTTS, "linePadding")
2771            .map(|s| s.to_string()),
2772        ebutts_multi_row_align: attribute_value(&node, NS_EBUTTS, "multiRowAlign")
2773            .map(|s| s.to_string()),
2774    }
2775}