Skip to content

Output functions

ProcessingModelFunctions is the abstract base every output format implements. The generated transform module calls its methods (paragraph, heading, block, inline, …) to emit output. Subclass it to add a new output format; see the concrete implementations for HTML, Print, EPUB, Markdown, Typst, DOCX and JSON.

Base class and helpers

opm.runtime.output_functions

Output format abstraction for TEI transformation.

Equivalent to html-functions.xql (and sibling format modules) in tei-publisher-lib/content. Each concrete subclass of ProcessingModelFunctions implements a specific serialisation target (HTML, Markdown, …). The generated transformation module calls methods on config.pmf and never imports a format-specific module directly.

HTML and Markdown implementations live in opm.html_output_functions and opm.markdown_output_functions; they are re-exported here for convenience.

TemplateOutput

Bases: str

Preformatted or template text; skip prose normalisation that collapses line breaks.

ProcessingModelFunctions

Bases: ABC

Abstract base for output format implementations.

Method names mirror the TEI Processing Model function vocabulary used by html-functions.xql / markdown-functions.xql etc. The generated transformation module calls these methods via config.pmf so that only the config construction needs to change when a different output format is required.

Every method receives config, a RenderContext, as its first argument. Output functions delegate recursive processing through it without importing the dispatch module:

config.applyapply(config, nodes) → list config.apply_childrenapply_children(config, node, content, parent) → None

A run creates its own instance, so per-run caches may live on self.

code

code(config, node, cls, content, language=None) -> PMResult

Fallback code behaviour for output modes without dedicated formatting.

Source code in src/opm/runtime/output_functions.py
def code(self, config, node, cls, content, language=None) -> PMResult:
    """Fallback code behaviour for output modes without dedicated formatting."""
    _ = language
    return self.pass_through(config, node, cls, content)

code_inline

code_inline(config, node, cls, content) -> PMResult

Inline code span — raw text, no markup escaping applied to the body.

Source code in src/opm/runtime/output_functions.py
def code_inline(self, config, node, cls, content) -> PMResult:
    """Inline code span — raw text, no markup escaping applied to the body."""
    return self.inline(config, node, cls, content)

unmatched

unmatched(config, node) -> PMResult

Handle an element no model matched, i.e. the generated case _: arm.

Default: recurse into the children, which is what the generated dispatch does inline for every other output mode. Only the JSON mode overrides this — it is the sole way to see elements the ODD has no model for, since they otherwise never reach a pmf method at all.

Source code in src/opm/runtime/output_functions.py
def unmatched(self, config, node) -> PMResult:
    """Handle an element no model matched, i.e. the generated ``case _:`` arm.

    Default: recurse into the children, which is what the generated dispatch
    does inline for every other output mode.  Only the JSON mode overrides
    this — it is the sole way to see elements the ODD has no model for,
    since they otherwise never reach a ``pmf`` method at all.
    """
    return config.apply(config, child_nodes(node))

finish

finish(config, nodes: list) -> list

Post-process output after apply, before footnotes.

Equivalent to pmf:finish in markdown-functions.xql / HTML siblings. Default: return nodes unchanged.

Source code in src/opm/runtime/output_functions.py
def finish(self, config, nodes: list) -> list:
    """Post-process output after [`apply`][opm.runtime.pm_runtime.apply], before footnotes.

    Equivalent to ``pmf:finish`` in ``markdown-functions.xql`` / HTML siblings.
    Default: return *nodes* unchanged.
    """
    return nodes

map_rend_to_class

map_rend_to_class(node)

Map @rend attribute tokens directly to CSS class names, e.g. 'bold' → 'bold'.

Source code in src/opm/runtime/output_functions.py
def map_rend_to_class(node):
    """Map @rend attribute tokens directly to CSS class names, e.g. 'bold' → 'bold'."""
    rend = node.get('rend')
    if rend:
        return ' '.join(rend.split())
    return None

classes

classes(*args)

Build a CSS class string, discarding None / empty entries.

Source code in src/opm/runtime/output_functions.py
def classes(*args):
    """Build a CSS class string, discarding None / empty entries."""
    return ' '.join(c for c in args if c)

add_lang_attrs

add_lang_attrs(el, source_node)

Copy @xml:lang from source_node as HTML lang/dir attributes on el.

Source code in src/opm/runtime/output_functions.py
def add_lang_attrs(el, source_node):
    """Copy @xml:lang from *source_node* as HTML lang/dir attributes on *el*."""
    lang = source_node.get(XML_LANG)
    if lang:
        base = lang.split('-')[0]
        el.set('lang', lang)
        el.set('dir', 'rtl' if base in RTL_LANGUAGES else 'ltr')

normalize

normalize(content)

Return content as a flat list of strings and lxml Elements.

Source code in src/opm/runtime/output_functions.py
def normalize(content):
    """Return content as a flat list of strings and lxml Elements."""
    if content is None:
        return []
    if isinstance(content, (str, etree._ElementUnicodeResult)):
        return [str(content)]
    if isinstance(content, etree._Element):
        return [content]
    # XPath atomics (number(), count unwrapped elsewhere, booleans) from ``xpath_content``
    if isinstance(content, (int, float, bool)):
        return [str(content)]
    return list(content)

child_nodes

child_nodes(node)

All child content as a flat list of strings and elements.

Equivalent to the XPath node() axis: preserves interleaved text and element children (including tail text of each child element).

Source code in src/opm/runtime/output_functions.py
def child_nodes(node):
    """All child content as a flat list of strings and elements.

    Equivalent to the XPath ``node()`` axis: preserves interleaved text and
    element children (including tail text of each child element).
    """
    result = []
    if node.text:
        result.append(node.text)
    for child in node:
        result.append(child)
        if child.tail:
            result.append(child.tail)
    return result

should_preserve_whitespace

should_preserve_whitespace(cls: list) -> bool

Return True when dispatch classes indicate preformatted / code content.

Source code in src/opm/runtime/output_functions.py
def should_preserve_whitespace(cls: list) -> bool:
    """Return True when dispatch classes indicate preformatted / code content."""
    for item in cls:
        if not item:
            continue
        for name in str(item).split():
            if name in _PRESERVE_WHITESPACE_CLASS_NAMES:
                return True
    return False

apply_children_without_normalization

apply_children_without_normalization(
    config: dict, source_node, content, parent_el
) -> None

Call config.apply_children with normalize_text off for this subtree.

Source code in src/opm/runtime/output_functions.py
def apply_children_without_normalization(
    config: dict,
    source_node,
    content,
    parent_el,
) -> None:
    """Call ``config.apply_children`` with ``normalize_text`` off for this subtree."""
    config.apply_children(config.derive(normalize_text=None), source_node, content, parent_el)

serialize_element_content_literal

serialize_element_content_literal(el: _Element) -> str

Serialize the mixed content inside el as literal XML/text.

Source code in src/opm/runtime/output_functions.py
def serialize_element_content_literal(el: etree._Element) -> str:
    """Serialize the mixed content inside *el* as literal XML/text."""
    parts: list[str] = []
    if el.text:
        parts.append(el.text)
    for child in el:
        if isinstance(child.tag, str):
            parts.append(_serialize_xml_element_local(child, with_tail=True))
        else:
            raw = etree.tostring(child, encoding='unicode')
            if isinstance(raw, bytes):
                raw = raw.decode('utf-8')
            parts.append(raw)
            if child.tail:
                parts.append(child.tail)
    return ''.join(parts)

literal_code_body

literal_code_body(node: _Element, content) -> str

Build a code-block body without running child elements through the PM.

Source code in src/opm/runtime/output_functions.py
def literal_code_body(node: etree._Element, content) -> str:
    """Build a code-block body without running child elements through the PM."""
    items = normalize(content)
    if len(items) == 1 and isinstance(items[0], etree._Element) and items[0] is node:
        return serialize_element_content_literal(node)
    parts: list[str] = []
    for item in items:
        if isinstance(item, str):
            parts.append(item)
        elif isinstance(item, etree._Element):
            parts.append(_serialize_xml_element_local(item, with_tail=True))
    return ''.join(parts)

join_eol_hyphen

join_eol_hyphen(s: str) -> str

Close up a word the source split across lines at a soft hyphen.

Where an encoder marks the split with U+00AD and indents the continuation (Arra­<lb/>gon), dropping the lb leaves that indentation behind, and every renderer collapses it to a space: "Arra gon". The soft hyphen is kept, so a reading system may still break the word there.

Source code in src/opm/runtime/output_functions.py
def join_eol_hyphen(s: str) -> str:
    """Close up a word the source split across lines at a soft hyphen.

    Where an encoder marks the split with U+00AD and indents the continuation
    (``Arra\u00ad<lb/>gon``), dropping the ``lb`` leaves that indentation behind,
    and every renderer collapses it to a space: "Arra gon". The soft hyphen is
    kept, so a reading system may still break the word there.
    """
    return _EOL_HYPHEN_RE.sub('\u00ad', s)

maybe_normalize_text

maybe_normalize_text(s: str, norm) -> str

Apply norm unless s is template output that must keep \n.

Source code in src/opm/runtime/output_functions.py
def maybe_normalize_text(s: str, norm) -> str:
    """Apply *norm* unless *s* is template output that must keep ``\\n``."""
    if isinstance(s, TemplateOutput):
        return s
    s = join_eol_hyphen(s)
    return norm(s) if norm else s

apply_pb_template

apply_pb_template(
    template_str: str,
    params: dict,
    config: dict | None = None,
) -> list

Execute a pb:template: parse template_str as XML, substitute [[param]] placeholders, and return the resulting list of nodes (strings and lxml Elements).

config is accepted for API symmetry but not currently used; element-valued params that appear in text positions are inserted directly into the result tree. The ODD compiler is expected to pass already-rendered output nodes (see apply_template_param_value, which expands a context node to processed children instead of raw TEI).

Source code in src/opm/runtime/output_functions.py
def apply_pb_template(template_str: str, params: dict, config: dict | None = None) -> list:
    """Execute a pb:template: parse *template_str* as XML, substitute [[param]] placeholders,
    and return the resulting list of nodes (strings and lxml Elements).

    *config* is accepted for API symmetry but not currently used; element-valued params that
    appear in text positions are inserted directly into the result tree. The ODD compiler is
    expected to pass already-rendered output nodes (see
    [`apply_template_param_value`][opm.runtime.pm_runtime.apply_template_param_value], which expands a context node
    to processed children instead of raw TEI).
    """
    wrapped = f'<__w__>{template_str}</__w__>'
    try:
        root = etree.fromstring(wrapped.encode('utf-8'))
    except etree.XMLSyntaxError:
        return []
    _substitute_node(root, params)
    result = []
    if root.text:
        result.append(root.text)
    for child in root:
        tail = child.tail
        child.tail = None
        result.append(child)
        if tail:
            result.append(tail)
    return _coerce_template_strings(result)

HTML

opm.runtime.html_output_functions.HtmlOutputFunctions

Bases: ProcessingModelFunctions

Serialise to HTML5 using lxml elements.

finish

finish(config, nodes: list) -> list

HTML pipeline has no finish normalisation; return nodes unchanged.

Source code in src/opm/runtime/html_output_functions.py
def finish(self, config, nodes: list) -> list:
    """HTML pipeline has no ``finish`` normalisation; return *nodes* unchanged."""
    return nodes

pass_through

pass_through(config, node, cls, content) -> PMResult

Render content without adding a wrapper element.

Source code in src/opm/runtime/html_output_functions.py
def pass_through(self, config, node, cls, content) -> PMResult:
    """Render content without adding a wrapper element."""
    result = []
    for item in normalize(content):
        if isinstance(item, str):
            result.append(item)
        elif isinstance(item, etree._Element):
            sub = (config.apply(config, child_nodes(node))
                   if item is node
                   else config.apply(config, [item]))
            result.extend(sub)
    return result

note

note(
    config, node, cls, content, place=None, label=None
) -> PMResult

Emit note - margin notes as inline spans, others as footnotes.

Source code in src/opm/runtime/html_output_functions.py
def note(self, config, node, cls, content, place=None, label=None) -> PMResult:
    """Emit note - margin notes as inline spans, others as footnotes."""
    node_id = node.get(XML_ID) or node.get('id') or str(config.state.note_counter + 1)
    safe_id = re.sub(r'[-.]', '_', node_id)

    # Margin notes: output inline span(s), not footnotes
    if place == 'margin':
        result = []
        if label:
            # Label reference span
            ref_span = self._el('span', list(cls) + ['margin-note-ref'], node)
            config.apply_children(config, node, [label], ref_span)
            result.append(ref_span)
            # Margin note content with label
            note_span = self._el('span', list(cls) + ['margin-note'], node)
            n_span = etree.SubElement(note_span, 'span')
            n_span.set('class', 'n')
            n_span.text = label if isinstance(label, str) else str(label)
            n_span.tail = ' '
            config.apply_children(config, node, content, note_span)
            result.append(note_span)
        else:
            # Margin note without label
            note_span = self._el('span', list(cls) + ['margin-note'], node)
            note_span.set('id', f'margin_ref_{safe_id}')
            config.apply_children(config, node, content, note_span)
            result.append(note_span)
        return result

    # Footnote handling (default)
    counter = config.state.next_note()
    nr = label if label is not None else counter

    ref_span = etree.Element('span')
    ref_span.set('id', f'fnref_{safe_id}')
    ref_span.set('style', 'display:inline-block')
    ref_span.set('class', classes(*cls))
    a = etree.SubElement(ref_span, 'a')
    a.set('class', 'note')
    a.set('rel', 'footnote')
    a.set('href', f'#fn_{safe_id}')
    a.text = str(nr)

    dl = etree.Element('dl')
    dl.set('class', 'footnote')
    dl.set('id', f'fn_{safe_id}')
    dt = etree.SubElement(dl, 'dt')
    dt.set('class', 'fn-number')
    dt.text = str(nr)
    dd = etree.SubElement(dl, 'dd')
    dd.set('class', 'fn-content')
    add_lang_attrs(dd, node)
    config.apply_children(config, node, content, dd)
    back = etree.SubElement(dd, 'a')
    back.set('class', 'fn-back')
    back.set('href', f'#fnref_{safe_id}')
    back.text = '↩'

    config.state.footnotes.append(dl)
    return [ref_span]

code

code(config, node, cls, content, language=None) -> PMResult

Emit a <pre><code> block so whitespace is preserved without JS.

Source code in src/opm/runtime/html_output_functions.py
def code(self, config, node, cls, content, language=None) -> PMResult:
    """Emit a ``<pre><code>`` block so whitespace is preserved without JS."""
    pre = self._el('pre', cls, node)
    code_el = etree.SubElement(pre, 'code')
    if language is not None and not isinstance(language, (list, tuple)):
        lang = str(language).strip()
        if lang:
            code_el.set('data-language', lang)
    config.apply_children(config, node, content, code_el)
    return [pre]

opm.runtime.print_output_functions.PrintOutputFunctions

Bases: HtmlOutputFunctions

Serialise to HTML tuned for paged-media CSS (Prince, Paged.js, print).

Equivalent to the pmf:* overrides in ext-printcss.xql.

note

note(
    config, node, cls, content, place=None, label=None
) -> PMResult

Emit note as an inline span for CSS float: footnote / margin notes.

Unlike HtmlOutputFunctions.note, does not build callout links or append bodies to config.state.footnotes. A label (TEI @n) is kept as data-n so print CSS can show a, b, … instead of the running number.

Source code in src/opm/runtime/print_output_functions.py
def note(self, config, node, cls, content, place=None, label=None) -> PMResult:
    """Emit note as an inline span for CSS ``float: footnote`` / margin notes.

    Unlike [`HtmlOutputFunctions.note`][opm.runtime.html_output_functions.HtmlOutputFunctions.note], does not build callout links or
    append bodies to ``config.state.footnotes``. A label (TEI ``@n``) is kept
    as ``data-n`` so print CSS can show a, b, … instead of the running number.
    """
    fn_class = 'margin-note' if place == 'margin' else 'footnote'
    el = self._el('span', list(cls) + [fn_class], node)
    if label is not None:
        items = label if isinstance(label, (list, tuple)) else [label]
        n = ' '.join(''.join(str(item) for item in items).split())
        if n:
            el.set('data-n', n)
    config.apply_children(config, node, content, el)
    return [el]

alternate

alternate(
    config,
    node,
    cls,
    content,
    default,
    alternate,
    optional=None,
) -> PMResult

Emit the default reading plus the alternate as a print footnote.

Ignores webcomponents / popovers — print has no interactive UI.

Source code in src/opm/runtime/print_output_functions.py
def alternate(self, config, node, cls, content, default, alternate, optional=None) -> PMResult:
    """Emit the default reading plus the alternate as a print footnote.

    Ignores ``webcomponents`` / popovers — print has no interactive UI.
    """
    _ = content, optional
    outer = self._el('span', cls, node)
    config.apply_children(config, node, default, outer)
    result: PMResult = [outer]
    if alternate is not None:
        result.extend(
            self.note(config, node, cls, alternate, place='footnote', label=None)
        )
    return result

EPUB

opm.runtime.epub_output_functions.EpubOutputFunctions

Bases: HtmlOutputFunctions

Serialise to HTML with EPUB 3 structural semantics.

Equivalent to the pmf:* overrides in ext-epub.xql. Packaging into a .epub ZIP is handled separately by opm.epub.

note

note(
    config, node, cls, content, place=None, label=None
) -> PMResult

Emit an EPUB noteref + footnote aside (in-flow; packager may hoist).

Source code in src/opm/runtime/epub_output_functions.py
def note(self, config, node, cls, content, place=None, label=None) -> PMResult:
    """Emit an EPUB noteref + footnote aside (in-flow; packager may hoist)."""
    _ = place, label
    nr = config.state.next_note()
    fn_id = _epub_safe_id(node, config)

    ref = etree.Element('a', nsmap={'epub': EPUB_NS})
    ref.set(EPUB_TYPE, 'noteref')
    ref.set('href', f'#fn{fn_id}')
    ref.set('class', 'noteref')
    ref.text = str(nr)

    aside = etree.Element('aside', nsmap={'epub': EPUB_NS})
    aside.set(EPUB_TYPE, 'footnote')
    aside.set('id', f'fn{fn_id}')
    aside.set('class', classes('note', *cls))
    add_lang_attrs(aside, node)
    aside.append(_footnote_body(config, node, content))
    return [ref, aside]

alternate

alternate(
    config,
    node,
    cls,
    content,
    default,
    alternate,
    optional=None,
) -> PMResult

Default reading as noteref; alternate body as footnote aside.

Source code in src/opm/runtime/epub_output_functions.py
def alternate(self, config, node, cls, content, default, alternate, optional=None) -> PMResult:
    """Default reading as noteref; alternate body as footnote aside."""
    _ = content, optional
    fn_id = _epub_safe_id(node, config)

    ref = etree.Element('a', nsmap={'epub': EPUB_NS})
    ref.set(EPUB_TYPE, 'noteref')
    ref.set('href', f'#fn{fn_id}')
    ref.set('class', classes('alternate', *cls))
    config.apply_children(config, node, default, ref)

    aside = etree.Element('aside', nsmap={'epub': EPUB_NS})
    aside.set(EPUB_TYPE, 'footnote')
    aside.set('id', f'fn{fn_id}')
    aside.set('class', classes('altcontent', *cls))
    aside.append(_footnote_body(config, node, alternate))
    return [ref, aside]

webcomponent

webcomponent(
    config, node, cls, content, name, optional=None
) -> PMResult

Degrade custom elements: EPUB 3 XHTML has no place for them.

pb-link keeps its cross-reference as a fragment link (rewritten to the target chapter file during packaging); everything else becomes a transparent div / span wrapper.

Source code in src/opm/runtime/epub_output_functions.py
def webcomponent(self, config, node, cls, content, name, optional=None) -> PMResult:
    """Degrade custom elements: EPUB 3 XHTML has no place for them.

    ``pb-link`` keeps its cross-reference as a fragment link (rewritten to
    the target chapter file during packaging); everything else becomes a
    transparent ``div`` / ``span`` wrapper.
    """
    opts = optional or {}
    if name == 'pb-code-highlight':
        return super().webcomponent(config, node, cls, content, name, optional)

    if name == 'pb-link':
        target = opts.get('xml-id') or opts.get('xml_id')
        if target:
            a = self._el('a', cls, node)
            a.set('href', f'#{target}')
            config.apply_children(config, node, content, a)
            return [a]

    el = self._el('div', cls, node)
    xml_id = node.get(XML_ID)
    if xml_id:
        el.set('id', xml_id)
    config.apply_children(config, node, content, el)
    if not any(
        isinstance(child.tag, str) and etree.QName(child).localname in BLOCK_TAGS
        for child in el
    ):
        el.tag = 'span'
    return [el]

cells

cells(config, node, cls, content) -> PMResult

Wrap each content item as a <td> inside a <tr> (ext-epub).

Source code in src/opm/runtime/epub_output_functions.py
def cells(self, config, node, cls, content) -> PMResult:
    """Wrap each content item as a ``<td>`` inside a ``<tr>`` (``ext-epub``)."""
    tr = etree.Element('tr')
    for item in normalize(content):
        td = etree.SubElement(tr, 'td')
        td.set('class', classes(*cls))
        config.apply_children(config, node, item, td)
    return [tr]

EPUB packaging (ZIP / OPF / nav) lives in opm.epub, not in the PMF.

Markdown

opm.runtime.markdown_output_functions.MarkdownOutputFunctions

Bases: ProcessingModelFunctions

Serialise to Markdown text fragments (CommonMark-style).

Mirrors pmf:* in tei-publisher-lib/content/markdown-functions.xql: paragraph breaks, headings with #, list markers, pipe tables, links, reference-style notes, and simple rend-based emphasis.

finish

finish(config, nodes: list) -> list

Run pmf:finish-style cleanup: collapse blank lines, tighten _ / ** spans.

Source code in src/opm/runtime/markdown_output_functions.py
def finish(self, config, nodes: list) -> list:
    """Run ``pmf:finish``-style cleanup: collapse blank lines, tighten ``_`` / ``**`` spans."""
    text = apply_markdown_finish_regexes(_serialize_pm_result(nodes))
    return [text]

Typst

opm.runtime.typst_output_functions.TypstOutputFunctions

Bases: ProcessingModelFunctions

Serialise to Typst markup text fragments.

DOCX

opm.runtime.docx_output_functions.DocxOutputFunctions

DocxOutputFunctions()

Bases: ProcessingModelFunctions

DOCX output for TEI processing model.

Style resolution rules (matching tei-publisher-lib): - For each ODD cssClass that does not start with tei-, look up the name (case-insensitive) in the template's paragraph / character / table style index. First match wins; unknown classes are silently skipped. - Paragraph fallback: "Normal". Character fallback: no style (CSS properties like bold/italic still apply). Table fallback: "TableGrid".

Source code in src/opm/runtime/docx_output_functions.py
def __init__(self) -> None:
    self._styles_loaded = False
    self._para_styles: dict[str, str] = {}
    self._char_styles: dict[str, str] = {}
    self._table_styles: dict[str, str] = {}
    self._bullet_numid: int = 1
    self._ordered_numid: int = 5
    self._bullet_abstract_id: int = 8   # python-docx default abstract for bullet
    self._ordered_abstract_id: int = 7  # python-docx default abstract for ordered
    self._needs_numbering: bool = False
    self._current_template: str | None = None  # Store template path globally
    # Per-run package state; a run creates its own instance.
    self._footnotes: dict[int, list] = {}
    self._num_instances: list[tuple[int, str]] = []
    self._image_counter = 0
    self._image_rid_map: dict[str, str] = {}

metadata

metadata(config, node, cls, content, key=None) -> PMResult

Collect a header value under key instead of emitting body content.

Mirrors TypstOutputFunctions.metadata: the ODD names the field, the collected text lands in config.state.metadata and is mapped onto the .docx core properties by finish.

Source code in src/opm/runtime/docx_output_functions.py
def metadata(self, config, node, cls, content, key=None) -> PMResult:
    """Collect a header value under *key* instead of emitting body content.

    Mirrors `TypstOutputFunctions.metadata`: the ODD names the field,
    the collected text lands in ``config.state.metadata`` and is
    mapped onto the ``.docx`` core properties by [`finish`][opm.runtime.docx_output_functions.DocxOutputFunctions.finish].
    """
    if key:
        self._ensure_styles(config)
        text = _ooxml_text(self._collect(config, node, content)).strip()
        config.state.metadata.setdefault(str(key), []).append(text)
    return []

finish

finish(config: dict, nodes: list) -> list

Assemble body elements into a .docx and return [bytes].

Source code in src/opm/runtime/docx_output_functions.py
def finish(self, config: dict, nodes: list) -> list:
    """Assemble body elements into a ``.docx`` and return ``[bytes]``."""
    from docx import Document  # noqa: PLC0415
    from docx.opc.part import Part  # noqa: PLC0415
    from docx.opc.packuri import PackURI  # noqa: PLC0415

    template_path = config.docx_template
    doc = Document(template_path) if template_path else Document()
    if not self._styles_loaded:
        self._load_style_index(doc)
    self._inject_missing_builtin_styles(doc)
    self._apply_core_properties(config, doc)

    if self._needs_numbering:
        import docx as _docx_pkg  # noqa: PLC0415
        import os  # noqa: PLC0415
        _default_path = os.path.join(os.path.dirname(_docx_pkg.__file__), 'templates', 'default.docx')
        _default_doc = Document(_default_path)
        _np = _default_doc.part.numbering_part
        from docx.opc.constants import RELATIONSHIP_TYPE as _RT  # noqa: PLC0415
        if doc.part.package:
            _new_np = type(_np)(_np.partname, _np.content_type, _np._element, doc.part.package)
            doc.part.relate_to(_new_np, _RT.NUMBERING)

    # Add per-list w:num instances so each list gets its own counter
    instances = self._num_instances
    if instances:
        numbering_el = doc.part.numbering_part._element
        # Create pStyle-free abstract copies so Word doesn't share a global
        # counter across instances (abstracts with w:pStyle bindings do that).
        bullet_abs_id, ordered_abs_id = self._add_pstyle_free_abstracts(numbering_el)
        kind_to_abs = {'bullet': bullet_abs_id, 'ordered': ordered_abs_id}
        for new_numid, kind in instances:
            num_el = _w('num')
            _wset(num_el, 'numId', str(new_numid))
            abs_ref = _wsub(num_el, 'abstractNumId')
            _wset(abs_ref, 'val', str(kind_to_abs[kind]))
            # Force counter restart at 1 — without this explicit override
            # Word may continue counting from a previous list.
            lvl_override = _wsub(num_el, 'lvlOverride')
            _wset(lvl_override, 'ilvl', '0')
            start_override = _wsub(lvl_override, 'startOverride')
            _wset(start_override, 'val', '1')
            numbering_el.append(num_el)

    body = doc.element.body
    sectPr = body.find(f'{{{W}}}sectPr')
    for child in list(body):
        if etree.QName(child).localname != 'sectPr':
            body.remove(child)

    # Inline content left at the top level (a page break's bare w:r, a
    # sentinel, loose text) must sit inside a w:p: Word rejects the file
    # when w:body holds runs directly.
    body_elements = self._blockify(self._filter_ooxml(nodes))
    doc_nsmap = doc.element.nsmap
    self._replace_footnote_sentinels(body_elements, doc_nsmap)
    self._replace_hyperlink_sentinels(body_elements, doc, doc_nsmap)
    self._replace_image_sentinels(body_elements, doc, doc_nsmap, config)

    for el in body_elements:
        if sectPr is not None:
            sectPr.addprevious(el)
        else:
            body.append(el)

    footnotes_data = self._footnotes
    if footnotes_data:
        # Drop any existing footnotes part from the template to avoid duplicates.
        existing_fn_rids = [
            rId for rId, rel in list(doc.part.rels.items())
            if rel.reltype == FOOTNOTES_RT
        ]
        for rId in existing_fn_rids:
            doc.part.rels.pop(rId)
        footnote_rels = self._resolve_footnote_sentinels(
            footnotes_data, doc, doc_nsmap, config
        )
        xml_bytes = self._build_footnotes_xml(footnotes_data)
        footnotes_part = Part(
            PackURI('/word/footnotes.xml'),
            FOOTNOTES_CT,
            xml_bytes,
            doc.part.package,
        )
        doc.part.relate_to(footnotes_part, FOOTNOTES_RT)

    self._drop_custom_xml_rels(doc)

    buf = BytesIO()
    doc.save(buf)

    buf = self._normalize_document_xml(buf)

    if footnotes_data:
        buf = self._inject_footnotes_rels(buf, footnote_rels)

    return [buf.getvalue()]

JSON

Records the processing model's own decisions rather than rendering the document. It is the only implementation that overrides unmatched, which the generated dispatch calls for elements no model matched.

opm.runtime.json_output_functions.JsonOutputFunctions

Bases: ProcessingModelFunctions

Emit the processing model's decisions as JSON records.

template

template(
    config, node, cls, template_str: str, params: dict
) -> PMResult

Record a pb:template and keep the content it wraps.

The generator hands templates their parameters already processed, so params['content'] is a list of finished records. Dropping it would lose every element a template-heavy ODD wraps — for DocBook that is most of the document.

Source code in src/opm/runtime/json_output_functions.py
def template(
    self, config, node, cls, template_str: str, params: dict,
) -> PMResult:  # type: ignore[override]
    """Record a ``pb:template`` and keep the content it wraps.

    The generator hands templates their parameters already processed, so
    ``params['content']`` is a list of finished records. Dropping it would
    lose every element a template-heavy ODD wraps — for DocBook that is
    most of the document.
    """
    _ = config
    children: list = []
    scalars: dict = {}
    for key, value in (params or {}).items():
        if isinstance(value, list):
            children.extend(
                item for item in value
                if isinstance(item, dict) or isinstance(item, str)
            )
        elif isinstance(value, dict):
            children.append(value)
        else:
            flat = _flatten(value)
            if flat is not None:
                scalars[key] = flat
    rec = self._build(
        node, cls, 'template', _prune(children), positions=_positions(config),
    )
    # Templates are indented XML blocks; collapse them so records stay legible.
    rec['template'] = ' '.join(template_str.split())
    if scalars:
        rec['params'] = scalars
    return [rec]

pass_through

pass_through(config, node, cls, content) -> PMResult

Recurse, but still record that a pass_through model won.

Emitting nothing here would hide a real decision: when the model you expected did not fire because a pass_through one matched first, the element simply would not appear. The same argument that puts inline and suppressed behaviours in the tree applies to this one.

Source code in src/opm/runtime/json_output_functions.py
def pass_through(self, config, node, cls, content) -> PMResult:
    """Recurse, but still record that a ``pass_through`` model won.

    Emitting nothing here would hide a real decision: when the model you
    expected did not fire because a ``pass_through`` one matched first,
    the element simply would not appear. The same argument that puts inline
    and suppressed behaviours in the tree applies to this one.
    """
    result: list = []
    for item in normalize(content):
        if isinstance(item, (str, dict)):
            result.append(item)
        elif isinstance(item, etree._Element):
            sub = (
                config.apply(config, child_nodes(node))
                if item is node
                else config.apply(config, [item])
            )
            result.extend(sub)
    return [
        self._build(
            node, cls, 'pass_through', _prune(result),
            positions=_positions(config),
        ),
    ]

unmatched

unmatched(config, node) -> PMResult

Record an element the ODD has no model for, then recurse into it.

The generated dispatch routes its case _: arm here for JSON output. Without this the element would never reach a pmf method and its text would surface in some ancestor with no provenance at all — which is the one thing you most want to find when an ODD looks incomplete.

Source code in src/opm/runtime/json_output_functions.py
def unmatched(self, config, node) -> PMResult:
    """Record an element the ODD has no model for, then recurse into it.

    The generated dispatch routes its ``case _:`` arm here for JSON output.
    Without this the element would never reach a ``pmf`` method and its
    text would surface in some ancestor with no provenance at all — which
    is the one thing you most want to find when an ODD looks incomplete.
    """
    children: list = []
    config.apply_children(config, node, node, children)
    return [self._build(node, [], None, _prune(children), positions=_positions(config))]