tei-core now models the top-level TeiDocument together with its
TeiHeader and body-aware TeiText. The text model records ordered
paragraphs (P), utterances with optional speaker references, and structural
divisions (Div) containing paragraphs, utterances, lists (List/Item,
where each Item may carry an optional inline Label), and nested
subdivisions. Each Div keeps a required @type (DivType), an optional
@subtype, an optional @xml:id, and an optional Head wrapper for a
single leading <head> element in the Episodic profile. Each block stores a
sequence of Inline nodes, allowing clients to mix plain text with emphasized
<hi> spans and <pause/> cues without hand-rolling XML. Plain strings
flow through P::from_text_segments, Utterance::from_text_segments,
Item::from_text_segments, Label::from_text, and Head::from_text; the
older new constructors remain as deprecated shims for existing callers
where applicable. TeiDocument now exposes validate() to enforce
document-wide rules: it rejects duplicate xml:id values across annotation
systems, paragraphs, utterances, divisions, lists, and items, including
nested divisions, and ensures utterance speakers appear in the profile cast
when it exists. An empty cast still counts as declared, so every who fails
until the speakers are populated, whereas the absence of a cast allows
speaker references, so drafts can be validated incrementally. Identifier
checks span the header as well, catching clashes between annotation systems
and body blocks. Violations surface as TeiError::Validation. Utterances and
list items now also carry local provenance and citation attributes where
applicable, and XML deserialization remains strict for <u> and <item>:
misspelt or unsupported attributes are rejected instead of being silently
discarded.
tei-xml depends on the core crate and now covers both directions of XML
flow. serialize_document_title(raw_title) still emits a <title> snippet,
parse_xml(xml) wraps quick-xml to materialize full TeiDocument values,
and emit_xml(&document) now uses a hybrid emitter: header and stand-off
sections are serialized via quick_xml, while body content is handwritten so
mixed inline content and structural divisions round-trip correctly. All
helpers return TeiError, so callers see consistent diagnostics whether
parsing malformed input or attempting to emit control characters that XML
forbids.
tei-serde centralizes JSON and MessagePack serialization, allowing the
rest of the workspace to depend on a stable wrapper API (tei_serde::json,
tei_serde::msgpack) instead of taking direct dependencies on serde_json
and rmp-serde. It also publishes a versioned JSON Schema snapshot for
TeiDocument under schemas/tei-document.schema.vX.Y.Z.json (with
schemas/tei-document.schema.json tracking the latest snapshot), generated
from the tei-core Rust types via schemars. Schema generation is gated
behind the optional tei-core Cargo feature json-schema so consumers that
do not need schema publication can avoid pulling in schemars.
tei-py now ships the tei_rapporteur PyO3 module. The exported Document
class wraps TeiDocument, validates titles via the Rust constructors, and
exposes a title getter plus an emit_title_markup convenience method. The
module also surfaces a top-level emit_title_markup function so Python
callers mirror the Rust helper without reimplementing validation rules. The
MessagePack bridge exposes both from_msgpack and to_msgpack for binary
interchange. Dictionary exchange is available via from_dict/to_dict,
powered by pyo3-serde, so Python built-ins can cross the FFI boundary
without detouring through JSON text. Phase 2.2 adds parse_xml/emit_xml
bindings that forward TEI strings directly to the tei-xml helpers. Python
can now parse canonical TEI without detouring through MessagePack, and
emission always routes through the same forbidden-character guardrails as the
Rust callers. Python-facing errors are surfaced as ValueError for content
issues and TypeError when callers pass the wrong objects to the bindings.
tei-test-helpers captures assertion helpers that multiple crates reuse in
their unit and behaviour-driven tests.
pyproject.toml configures maturin to build tei-py, allowing
maturin develop or maturin build to work from the workspace root without
additional arguments.