transform¶
The high-level transform API. Import these to run a compiled transform module against parsed XML.
opm.transform ¶
Core transform API — import this to drive transforms from your own Python scripts.
Most callers want opm.Project.transform, which also compiles the
ODD and keeps the loaded module and registers between calls. The functions
here are the layer below it, at increasing levels of abstraction.
run_transform(mod, element, ...) is the lowest
level. The caller supplies an already-loaded module and an already-selected
lxml element; the function serializes and optionally wraps the result in the
Jinja2 document template:
from opm.odd_cache import ensure_compiled_module
from opm.resources import packaged_odd
from opm.transform import load_transform_module, run_transform
from lxml import etree
path, _ = ensure_compiled_module(packaged_odd('teipublisher'))
mod = load_transform_module(path)
root = etree.parse('document.xml').getroot()
html = run_transform(mod, root) # full document
fragment = run_transform(mod, root.find('.//{*}div')) # single element
transform_node(script_path, root, *, xpath=None, ...)
is the mid level. It loads the module from script_path and, if xpath is
given, selects the target element before transforming:
from opm.odd_cache import ensure_compiled_module
from opm.resources import packaged_odd
from opm.transform import transform_node
from lxml import etree
path, _ = ensure_compiled_module(packaged_odd('teipublisher'))
root = etree.parse('document.xml').getroot()
html = transform_node(path, root, xpath='//body/div[1]')
transform_file(module, xml_path, *, xpath=None, ...)
is the highest level, and what opm transform runs. It also parses the XML
file and takes everything else from opm.toml: parameters, registers,
extensions, web components, template:
from opm.odd_cache import ensure_compiled_module
from opm.resources import packaged_odd
from opm.transform import transform_file
path, _ = ensure_compiled_module(packaged_odd('teipublisher'))
html = transform_file(path, Path('document.xml'), xpath='//body/div[1]')
xpath_select(root, expr, ...) evaluates XPath
against a parsed document without any namespace bookkeeping: unprefixed names
automatically match the document's namespace:
from opm.transform import xpath_select
from lxml import etree
root = etree.parse('document.xml').getroot()
chapters = xpath_select(root, '//body/div')
TemplateArguments ¶
template_arguments ¶
template_arguments(
mode: OutputMode,
config: ProjectConfig,
override: Path | None = None,
) -> TemplateArguments
The template argument run_transform takes for a run in mode.
override (--template) wins over the project's
[transform.<type>] template. Without either, an HTML or Typst shell
falls back to its packaged default inside run_transform, and DOCX
to the packaged Word style template. Modes that take no template get none.
Source code in src/opm/transform.py
template_assets ¶
template_assets(
mode: OutputMode, config: ProjectConfig
) -> tuple[Path, ...]
The files mode's template references, e.g. [transform.web] assets.
Whoever writes the output copies them into an assets/ directory beside
it (see copy_assets); the template links them
from there.
Source code in src/opm/transform.py
load_transform_module ¶
Load a .py file that defines transform() and OUTPUT_MODE.
Source code in src/opm/transform.py
xpath_select ¶
xpath_select(
root: _Element,
expr: str,
params: dict[str, str] | None = None,
xpath_extensions: Sequence[str] | None = None,
xpath_base_uri: str | None = None,
xpath_documents: dict[str, Any] | None = None,
xpath_collections: dict[str, list] | None = None,
xpath_variables: dict[str, Any] | None = None,
xpath_namespaces: dict[str, str] | None = None,
*,
xpath_env: XPathEnvironment | None = None,
) -> list
Evaluate XPath 3.1 expr against root, returning a plain list.
The document's default namespace URI (taken from root's nsmap) is set
as the XPath default element namespace, so unprefixed element names match
without any prefix mapping:
chapters = xpath_select(root, '//body/div') # TEI, DocBook, …
titles = xpath_select(root, '//div/head/string()') # atomic results
Element results are returned as lxml _Element objects.
Atomic expressions (count(…), string(…)) return the corresponding
Python scalar.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
_Element
|
Document root element; its namespace determines the default element namespace. |
required |
expr
|
str
|
XPath 3.1 expression with unprefixed element names. |
required |
params
|
dict[str, str] | None
|
Values bound as the XPath |
None
|
xpath_extensions
|
Sequence[str] | None
|
Dotted module paths for custom XPath functions in the |
None
|
xpath_env
|
XPathEnvironment | None
|
Evaluate in this environment instead of one built from the other arguments; its cached document trees are then reused. |
None
|
Source code in src/opm/transform.py
load_xpath_documents ¶
Parse configured XPath documents keyed by their absolute file URI.
Each tree is wrapped in its elementpath node tree once, here, rather than
left as a bare _ElementTree. XPathContext.__init__ runs
get_node_tree() over every entry of documents on each
construction, and for a bare lxml tree that means a full
build_lxml_node_tree() walk every time — with a context built per
doc()-using predicate, the register documents were being re-wrapped
thousands of times per chunked file. get_node_tree() short-circuits on
an already-wrapped DocumentNode, so pre-wrapping turns that back into a
dict lookup. Mirrors what wrapped
does for the main document.
Source code in src/opm/transform.py
load_xpath_collections ¶
load_xpath_collections(
collections: Sequence[CollectionConfig],
documents: dict[str, Any] | None = None,
) -> tuple[dict[str, list], dict[str, Any]]
Return (collections, documents) maps for the XPath dynamic context.
Each member document is parsed and wrapped once (see
load_xpath_documents) and registered in both returned maps. The
second registration is not redundant: fn:id resolves its target document
through XPathContext.get_root(), which searches root and
documents but never collections. Without it,
collection($uri)/id($key) returns the empty sequence — silently, with no
error — which is the shape most register lookups take.
documents is merged into (and takes precedence in) the returned document
map, so a file listed both in [transform] documents and in a collection
is parsed once and shared as the same node object.
Source code in src/opm/transform.py
load_project_documents ¶
load_project_documents(
config: ProjectConfig,
) -> tuple[dict[str, Any], dict[str, list]]
(documents, collections) for the XPath dynamic context, from config.
Parses [transform] documents and every [[transform.collections]]
member once. Pass the result to project_xpath_env to share the
parsed registers across several documents.
Source code in src/opm/transform.py
project_xpath_env ¶
project_xpath_env(
config: ProjectConfig,
xml_path: Path | None = None,
*,
extensions: Sequence[str] | None = None,
documents: tuple[dict[str, Any], dict[str, list]]
| None = None,
) -> XPathEnvironment
The XPath environment a run over xml_path evaluates in.
Registers, collections, variables, namespaces and extension modules all
come from config; extensions replaces the configured modules when
given. xml_path is the base URI doc() resolves against. documents
is a load_project_documents result to reuse instead of parsing the
registers again.
The config's [project] pythonpath goes on sys.path first, so its
extension modules import from the Python API as they do from the CLI.
Source code in src/opm/transform.py
pb_links_to_anchors ¶
pb_links_to_anchors(
tree: _Element,
resolve: Callable[[str], str | None] = lambda href: (
href
),
) -> bool
Turn the pb-link elements below tree into plain anchors.
A standalone page has no pb-view listening on the channel a
pb-link emits on, so a link that only names its target in
xml-id/node-id/hash goes nowhere. Each such target becomes
#id, passed through resolve (which may point it at another file,
or return None to leave the element alone). A pb-link with no
target (the path-based browse form) is left untouched. Returns
whether anything changed.
Source code in src/opm/transform.py
run_transform ¶
run_transform(
mod: ModuleType,
root: _Element,
*,
parameters: dict[str, str] | None = None,
webcomponents: bool = False,
apply_template: bool = True,
template_path: Path | None = None,
template_context: dict[str, Any] | None = None,
assets: Sequence[Path] = (),
docx_template: Path | None = None,
typst_template_path: Path | None = None,
epub_chunking: ChunkingConfig | None = None,
epub_css: Path | None = None,
epub_skip_title: bool = False,
pages: str | None = None,
fragments: dict[str, str] | None = None,
footnote_order: str | Callable | None = None,
xpath_env: XPathEnvironment | None = None,
) -> str | bytes
Run mod against root and return the serialized output.
For HTML output, if the result contains a full <html> document and
apply_template is True, the result is wrapped in the Jinja2 document
template. Fragment transforms (e.g. a single <div>) skip this step.
For DOCX / EPUB output, returns raw bytes (the package file content).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mod
|
ModuleType
|
A loaded transform module (from |
required |
root
|
_Element
|
The lxml element to transform. |
required |
parameters
|
dict[str, str] | None
|
XPath |
None
|
webcomponents
|
bool
|
Enable TEI Publisher web-component mode. |
False
|
apply_template
|
bool
|
Wrap full-document HTML output in the Jinja2 template. |
True
|
template_path
|
Path | None
|
Override Jinja2 template (default: packaged template). |
None
|
template_context
|
dict[str, Any] | None
|
Project |
None
|
assets
|
Sequence[Path]
|
Files the HTML template references, expected in an
|
()
|
docx_template
|
Path | None
|
Path to a |
None
|
typst_template_path
|
Path | None
|
Jinja2 template for Typst document shell. |
None
|
epub_chunking
|
ChunkingConfig | None
|
Chapter selection for |
None
|
epub_css
|
Path | None
|
Stylesheet appended last to the EPUB package. |
None
|
epub_skip_title
|
bool
|
Omit the generated EPUB title page. |
False
|
pages
|
str | None
|
Dotted path of a page splitter ( |
None
|
fragments
|
dict[str, str] | None
|
Rendered fragments handed to the HTML template as
|
None
|
footnote_order
|
str | Callable | None
|
Orders each run's footnotes before they are placed
( |
None
|
xpath_env
|
XPathEnvironment | None
|
The XPath environment to evaluate in (see
|
None
|
Source code in src/opm/transform.py
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 | |
transform_node ¶
transform_node(
script_path: Path | ModuleType,
root: _Element,
*,
xpath: str | None = None,
parameters: dict[str, str] | None = None,
webcomponents: bool = False,
apply_template: bool = True,
template_path: Path | None = None,
template_context: dict[str, Any] | None = None,
assets: Sequence[Path] = (),
docx_template: Path | None = None,
typst_template_path: Path | None = None,
epub_chunking: ChunkingConfig | None = None,
epub_css: Path | None = None,
epub_skip_title: bool = False,
pages: str | None = None,
fragments: dict[str, str] | None = None,
footnote_order: str | Callable | None = None,
xpath_env: XPathEnvironment | None = None,
) -> str | bytes
Load script_path as a transform module and apply it to root.
If xpath is given it is evaluated against root via
resolve_element to select the
actual element to transform; unprefixed names use the document's default
namespace. Without xpath, root itself is the transform target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script_path
|
Path | ModuleType
|
Path to the compiled transform |
required |
root
|
_Element
|
The lxml element that acts as the document root for XPath
evaluation and (when xpath is |
required |
xpath
|
str | None
|
XPath 3.1 expression selecting a single child element to transform instead of root. |
None
|
parameters
|
dict[str, str] | None
|
XPath |
None
|
webcomponents
|
bool
|
Enable TEI Publisher web-component mode. |
False
|
apply_template
|
bool
|
Wrap full-document HTML output in the Jinja2 template. |
True
|
template_path
|
Path | None
|
Override Jinja2 template (default: packaged template). |
None
|
template_context
|
dict[str, Any] | None
|
Project |
None
|
assets
|
Sequence[Path]
|
Files the HTML template references (see
|
()
|
pages
|
str | None
|
Page splitter for HTML output (see
|
None
|
fragments
|
dict[str, str] | None
|
Rendered fragments for the HTML template (see
|
None
|
footnote_order
|
str | Callable | None
|
Footnote ordering for each run (see
|
None
|
xpath_env
|
XPathEnvironment | None
|
The XPath environment to evaluate in (see
|
None
|
Source code in src/opm/transform.py
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 | |
transform_file ¶
transform_file(
module: Path | ModuleType,
xml_path: Path,
*,
xpath: str | None = None,
parameters: dict[str, str] | None = None,
xpath_extensions: Sequence[str] | None = None,
webcomponents: bool | None = None,
template: Path | None = None,
config: ProjectConfig | None = None,
documents: tuple[dict[str, Any], dict[str, list]]
| None = None,
) -> str | bytes
Transform xml_path (or an XPath-selected element within it) with the project's settings.
This is what opm transform runs. Everything the arguments leave open
comes from the project config: $parameters, registers and collections,
XPath variables and extensions, web components, the template and its
context, and the EPUB chapter selection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
module
|
Path | ModuleType
|
The compiled transform module, or the path to its |
required |
xml_path
|
Path
|
Path to the XML input file. |
required |
xpath
|
str | None
|
XPath 3.1 expression selecting the transform root element. Unprefixed names use the document's default namespace. |
None
|
parameters
|
dict[str, str] | None
|
XPath |
None
|
xpath_extensions
|
Sequence[str] | None
|
Dotted module paths for custom XPath functions.
|
None
|
webcomponents
|
bool | None
|
Enable web-component mode.
|
None
|
template
|
Path | None
|
Template override (see |
None
|
config
|
ProjectConfig | None
|
Pre-loaded |
None
|
documents
|
tuple[dict[str, Any], dict[str, list]] | None
|
A |
None
|
Returns str for text output modes (HTML, Markdown, Typst) and bytes
for binary ones (DOCX, EPUB). To get a PDF, pass Typst output to
opm.typst_compile.compile_pdf.
Source code in src/opm/transform.py
transform_with_config ¶
transform_with_config(
mod: ModuleType,
root: _Element,
xml_path: Path | None,
config: ProjectConfig,
*,
xpath: str | None = None,
parameters: dict[str, str] | None = None,
xpath_extensions: Sequence[str] | None = None,
webcomponents: bool | None = None,
template: Path | None = None,
documents: tuple[dict[str, Any], dict[str, list]]
| None = None,
) -> str | bytes
Transform the already parsed root with config's settings.
The part of transform_file after parsing, shared with
opm.project.Project.transform. xml_path is the file root was
read from, if any: doc() resolves against it and it becomes
$parameters?input_path.
Source code in src/opm/transform.py
expand_placeholders ¶
Fill in the {…} placeholders a whole-document run can resolve.
{prefix} is the path back to the directory holding assets/: empty,
since opm transform writes its output right beside it. {doc} and
{doc_stem} name the source file. Chunk-only placeholders ({file},
{stem}) are left as they are, as is any other brace.
Source code in src/opm/transform.py
global_fragments ¶
global_fragments(
mod: ModuleType,
root: _Element,
config: ProjectConfig,
parameters: dict[str, Any],
env: XPathEnvironment,
*,
webcomponents: bool = False,
footnote_order: str | Callable | None = None,
) -> dict[str, str]
Render the scope = "global" fragments of [chunking] for root.
These describe the document as a whole (its title, a table of contents),
so a whole-document page can show them just as every chunk page does. A
fragment with an odd of its own is rendered with that ODD, otherwise
with mod. Only the first [[chunking]] run's fragments are used.
Source code in src/opm/transform.py
Templates¶
Rendering transform output through Jinja2 templates, and the frontmatter those templates may carry.
opm.template_rendering ¶
Helpers for wrapping transform output in Jinja2 templates (HTML and Typst).
TrimmedBlocks ¶
A template's blocks, each rendered on first use and trimmed.
Templates get one through the trimmed global: {% set blocks =
trimmed(self) %}, then blocks.before is the rendered before
block, an empty string when it holds only whitespace. That lets a layout
test whether a region has content before it writes the region's wrapper.
Source code in src/opm/template_rendering.py
default_template_path ¶
Return the path to a packaged default document template.
resolve_template_path ¶
resolve_template_path(
template_path: Path | None,
*,
default_name: str = DEFAULT_TEMPLATE_NAME,
) -> Path
Resolve override template path or packaged default template.
Source code in src/opm/template_rendering.py
layout_css ¶
The stylesheet behind opm/layout.html.j2.
Chunking writes it to css/opm-layout.css and passes layout_css_url;
a single transformed page has no such file, so the layout inlines it.
A copy at opm/opm-layout.css under templates_dir wins over the
packaged one, as the template loader does for the inlined case.
Source code in src/opm/template_rendering.py
packaged_templates_dir ¶
template_environment ¶
A Jinja2 environment for rendering template_path.
Templates are looked up in template_path's directory first, then among
the packaged ones, so a project template can {% extends
"opm/layout.html.j2" %} and can replace a packaged file by giving its
own the same relative path. Frontmatter is removed from every source and
feeds the template context (see
template_context). The
global trimmed gives access to a template's rendered blocks (see
TrimmedBlocks).
Source code in src/opm/template_rendering.py
load_template ¶
Load template_path from env, raising FileNotFoundError if missing.
Source code in src/opm/template_rendering.py
is_full_html_document ¶
Whether serialized output contains a full <html> document element.
render_document_template ¶
render_document_template(
*,
serialized_html: str,
template_path: Path,
odd_css: str | None,
parameters: dict[str, str],
context: dict | None = None,
base_css: str | None = None,
assets: str = "",
asset_styles: list[str] | None = None,
fragments: dict[str, str] | None = None,
) -> str
Render a full HTML document through the selected Jinja2 template.
context is the project's [context] table (see
context_for), exposed to the template
under the single name context so a project key can never shadow a
built-in like content_html.
base_css is a packaged baseline stylesheet (e.g. the print channel's
paged-media rules) rendered via its own {{ base_css }} template slot,
kept separate from odd_css rather than concatenated into it — so the
<style> that HtmlOutputFunctions.document() embedded in the
document's own <head> still matches odd_css exactly. That element is
removed from head_html; the template gets the stylesheet as
odd_css instead and decides where it goes.
assets and asset_styles are the URL prefix of the copied
[transform.web] assets and the stylesheets among them, the same
values chunk pages receive.
fragments are the rendered global fragments of [chunking] (the
title, a table of contents), under the same fragments name chunk pages
get them, so one template serves both.
Source code in src/opm/template_rendering.py
render_index_template ¶
render_index_template(
*,
entries: list,
template_path: Path,
title: str,
odd_css: str | None = None,
odd_css_url: str = "",
assets: str = "",
asset_styles: list[str] | None = None,
layout_css_url: str = "",
context: dict | None = None,
) -> str
Render a collection index listing chunked documents.
The template receives documents (a list of
IndexEntry), title, the odd_css string, the
odd_css_url file chunking writes, and — when
an assets setting is configured — an assets URL prefix plus
asset_styles, the stylesheets among those assets in declared order. The
index sits at the output root, so those URLs carry no ../ prefix.
Each entry exposes fragments, so a project controls what the index shows
by declaring global fragments in opm.toml — no code change needed to add
an author or date column.
Source code in src/opm/template_rendering.py
render_typst_document_template ¶
render_typst_document_template(
*,
content_typst: str,
template_path: Path,
odd_typst: str | None,
parameters: dict[str, str],
metadata: dict | None = None,
context: dict | None = None,
) -> str
Render Typst body content through the selected Jinja2 document shell.
Source code in src/opm/template_rendering.py
opm.template_frontmatter ¶
Frontmatter in Jinja2 templates, merged into the template context.
A template may open with a block of JSON or TOML between two --- lines,
as in jinks::
{# SPDX header comments may come first #}
---toml
styles = ["https://fonts.googleapis.com/css2?family=Belleza"]
[layout.after]
width = "22rem"
---
{% extends "opm/layout.html.j2" %}
The block is removed before Jinja2 sees the source. Its values are merged over
the packaged defaults (opm/defaults.toml) and the project's [context]
along the template's {% extends %} chain: the root layout first, the
rendered template last, so a child overrides its parent and frontmatter
overrides opm.toml. See
merge_deep for how values combine.
FrontmatterLoader ¶
Bases: FileSystemLoader
A FileSystemLoader that removes frontmatter and remembers it.
Source code in src/opm/template_frontmatter.py
frontmatter ¶
The frontmatter of template, loading it if need be.
Source code in src/opm/template_frontmatter.py
chain ¶
template and the templates it extends, root layout first.
Source code in src/opm/template_frontmatter.py
context ¶
The frontmatter of template's extends chain, merged root first.
Source code in src/opm/template_frontmatter.py
split_frontmatter ¶
Return the frontmatter of source as a dict, and the source without it.
The block is replaced by a Jinja2 comment spanning as many lines, so it
outputs nothing and line numbers in Jinja2 error messages still match the
file. ---json and a bare ---
hold JSON, ---toml holds TOML. A source without frontmatter comes back
unchanged with an empty dict.
Raises:
| Type | Description |
|---|---|
ValueError
|
The block names another syntax or does not parse. |
Source code in src/opm/template_frontmatter.py
merge_deep ¶
Merge maps left to right, the way jinks merges context maps.
- A scalar replaces an earlier value under the same key.
- Maps merge recursively. A map holding
"$replace": truereplaces the earlier map instead;"$replace": ["a", "b"]replaces only those keys and merges the rest. - Arrays are concatenated with duplicates dropped: maps with an
idcompare by it, other values by their JSON form.
$replace itself never ends up in the result.
Source code in src/opm/template_frontmatter.py
template_context ¶
template_context(
environment: Environment,
template: str,
base: Mapping[str, Any] | None = None,
) -> dict[str, Any]
The context to render template with: base, then its frontmatter.
base is the project's context ([context] overlaid by
[transform.<type>.context]). It is merged over the packaged
context_defaults, and the
frontmatter along the extends chain over both, with
merge_deep.
An environment whose loader is not a
FrontmatterLoader adds
neither.
Source code in src/opm/template_frontmatter.py
context_defaults ¶
The packaged defaults beneath every template context.
Read from opm/defaults.toml among the packaged templates: the settings
the packaged layouts understand, such as layout.after.width. A project
overrides them in [context] and a template in its frontmatter.
Source code in src/opm/template_frontmatter.py
Assets¶
opm.assets ¶
Files a web template references, copied next to the pages that use them.
A project lists them under [transform.web] assets (or [chunking]
assets for one chunking run). opm chunk and opm transform copy them
into an assets/ directory beside their output and hand the template the
stylesheets among them as asset_styles.
resolve_assets ¶
Expand assets entries to the paths to copy.
An entry holding *, ? or [ is matched against the filesystem, so
iiif/* copies every document's directory in one line instead of naming
each one — and keeps working when a document is added. Matches are sorted,
which fixes the cascade order of any stylesheets among them. Every other
entry is taken literally. Relative entries resolve against root.
A literal path that does not exist, or a pattern matching nothing, raises
FileNotFoundError. The alternative is output quietly missing a file a
template or model expects, which surfaces much later as a 404.
Source code in src/opm/assets.py
copy_assets ¶
Copy assets into <output_root>/assets/ and return their sources.
Each entry keeps its own name, a directory its whole tree. Copying again over an earlier run is safe.
Source code in src/opm/assets.py
asset_styles ¶
URLs of the stylesheets among sources, in the order they were declared.
That is the cascade order, so a template can link them blind. prefix is
the path from the page back to the output root (../ for a chunk page).