ttml-subtitle 0.2.0

TTML2 / IMSC 1.1 timed-text subtitle parser — W3C TTML2 + IMSC 1.1 profile validation.
Documentation

ttml-subtitle

Crates.io docs.rs MSRV License

TTML2 / IMSC 1.1 timed-text subtitle parser for Rust.

Parses W3C Timed Text Markup Language 2 (TTML2) documents and validates them against IMSC 1.1 Text Profile and Image Profile constraints. Parse a document, then validate it separately — the two passes are independent.

  • Spec: W3C TTML2 Recommendation (08 Nov 2018) + IMSC 1.1 Recommendation (08 Nov 2018, edited 27 Apr 2020)
  • MSRV: 1.95.0
  • License: MIT OR Apache-2.0

Quick Start

use ttml_subtitle::Document;

let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<tt xml:lang="en" xmlns="http://www.w3.org/ns/ttml"
   xmlns:ttp="http://www.w3.org/ns/ttml#parameter"
   ttp:contentProfiles="http://www.w3.org/ns/ttml/profile/imsc1.1/text">
  <body><div><p begin="0s" end="5s">Hello</p></div></body>
</tt>"#;

let doc = Document::parse_str(xml).unwrap();
let body = doc.tt.body.as_ref().unwrap();
assert_eq!(body.divs[0].paragraphs[0].begin.as_deref(), Some("0s"));

Features

  • Full TTML2 element tree (26 element types, 56 style properties)
  • Exhaustive <time-expression> grammar (clock-time, offset-time, wallclock-time) with frame/tick/SMPTE constraint enforcement
  • IMSC 1.1 profile validation (159-row feature disposition table, §7.12 "must reject" constraints)
  • Parse/validate split — inspect non-conformant documents before deciding to reject
  • Semantic round-trip (see Round-Trip Guarantee) — parse → serialize → re-parse yields an equal document
  • No raw-passthrough in the serializer — output is generated from typed fields
  • Real-fixture tested against 11 W3C IMSC conformance suite documents
  • #[no_std] + alloc compatible (with std feature)
  • Optional serde support

Examples

cargo run -p ttml-subtitle --example parse_document
cargo run -p ttml-subtitle --example validate_document
cargo run -p ttml-subtitle --example from_scratch

Round-Trip Guarantee

The crate provides semantic round-trip fidelity, not byte-identity. Parsing a document, serializing the typed model, and re-parsing yields an equal document (parse → serialize → re-parse → equal). But the serialized XML will differ from the input in the following ways (each verified against roxmltree's actual API, not assumed):

Category Status Reason
XML declaration Lost (roxmltree) roxmltree skips <?xml version="1.0" encoding="UTF-8"?>; the serializer always emits <?xml version="1.0" encoding="UTF-8"?>
Comments Lost (parser) roxmltree does expose comments (is_comment(), text()), but the parser does not store them
Processing instructions Lost (parser) roxmltree does expose PIs (NodeType::PI), but the parser does not store them
other_attributes (custom attributes not explicitly modeled) Lost (serializer) Parsed into BTreeMap<(ns, local), value> per element, but the serializer does not emit them — it only serializes known, explicitly modeled attributes
Foreign/unknown child elements (elements not in this crate's explicitly modeled set, whether in a foreign namespace or an unrecognized tt: element) Lost (parser) Silently skipped during parsing (allowed by TTML2 §7.2's "foreign namespace" extensibility rule) — never stored, so they cannot be re-emitted
Attribute order Deterministic, not preserved (parser) roxmltree does preserve document order via attributes(), but the parser collects known attrs into named fields and serializes them in a fixed code order
Namespace prefix spelling Deterministic, not preserved (roxmltree + serializer) roxmltree resolves QNames to (namespace_uri, local_name) pairs; the original prefix per-attribute is not exposed. The serializer uses fixed prefixes (tts:, ttp:, itta:, etc.)
Whitespace / indentation Deterministic, not preserved Indentation is always 2-space, closing tags always on new lines; interstitial whitespace-only text nodes are preserved by the parser but may differ in placement on re-serialization

The invariant that must hold: a document constructed entirely via the public types (no parsed input) will serialize and re-parse to an equal document. This is tested by the from_scratch example.

Profile Validation

use ttml_subtitle::{Document, validation::{Validator, Profile, ImscVersion}};

let doc = Document::parse_str(xml).unwrap();

let validator = Validator::new(Profile::Text, ImscVersion::V1_1);
let result = validator.validate(&doc);
if result.valid {
    println!("Valid IMSC 1.1 Text Profile document");
} else {
    for err in &result.errors {
        println!("{}: {}", err.constraint, err.detail);
    }
}