In-memory graph of TEI ODD specification elements.
This is the documentation counterpart of
opm.odd_compiler.parse_odd: that module merges
elementSpecs so they can be compiled, while this one indexes
elementSpec / classSpec / macroSpec / dataSpec and inverts
their class memberships and content-model references, from which
opm.odd_expand derives what the TEI Stylesheets show as
"Contained by", "May contain", "Members" and "Used by".
The input is a compiled TEI document or ODD (p5subset.xml, a full
Guidelines p5.xml, or the output of
compile_schema). Nested TEI in desc,
gloss, remarks and exemplum is left as lxml so a processing-model
transform can render it.
SpecRef
dataclass
SpecRef(ident: str, kind: str, module: str | None = None)
A pointer to another spec, used in membership / content lists.
AttDefView
dataclass
AttDefView(ident: str, node: _Element)
An attribute a spec declares itself.
SpecIndex
SpecIndex(
specs: dict[str, Spec],
*,
lang: str = "en",
title: str = "",
)
The specs of a schema and the direct facts about them, indexed.
The primitives: specs by ident, direct class memberships and content-model
references, both inverted so that "who is a member of X" and "who refers
to X" are lookups rather than scans, and the two graph walks everything
else is built from. What those facts mean on a documentation page —
contained-by, may-contain, used-by, attribute inheritance — is derived on
top, in opm.odd_expand. It is the counterpart of the
indexes eXist gave the XQuery version of this code.
Source code in src/opm/spec_index.py
| def __init__(
self,
specs: dict[str, Spec],
*,
lang: str = 'en',
title: str = '',
):
self._specs = specs
self.lang = lang
self.title = title
self._build_lookups()
|
attributes
attributes() -> list[tuple[str, list[Spec]]]
Attribute ident → specs (classes/elements) that define it, A–Z.
Source code in src/opm/spec_index.py
| def attributes(self) -> list[tuple[str, list[Spec]]]:
"""Attribute ident → specs (classes/elements) that define it, A–Z."""
owners: dict[str, list[Spec]] = defaultdict(list)
for spec in self.all():
if spec.kind not in {'class', 'element'}:
continue
for att in spec.local_atts:
owners[att.ident].append(spec)
return sorted(owners.items(), key=lambda kv: kv[0].lower())
|
members_of
members_of(key: str) -> list[SpecRef]
Specs whose memberOf names key directly, A–Z.
Source code in src/opm/spec_index.py
| def members_of(self, key: str) -> list[SpecRef]:
"""Specs whose ``memberOf`` names *key* directly, A–Z."""
return unique_refs([self.ref(ident) for ident in self._members_of.get(key, ())])
|
referrers
referrers(kind: str, key: str) -> list[SpecRef]
Specs whose content model refers to key directly.
kind is the reference: element (elementRef), class
(classRef), macro (macroRef) or data (dataRef).
In document order, duplicates kept, as the references stand.
Source code in src/opm/spec_index.py
| def referrers(self, kind: str, key: str) -> list[SpecRef]:
"""Specs whose content model refers to *key* directly.
*kind* is the reference: ``element`` (``elementRef``), ``class``
(``classRef``), ``macro`` (``macroRef``) or ``data`` (``dataRef``).
In document order, duplicates kept, as the references stand.
"""
return list(self._referrers.get((kind, key), ()))
|
content_refs
content_refs(ident: str) -> list[tuple[str, str]]
(kind, key) for every reference in ident's content model.
Source code in src/opm/spec_index.py
| def content_refs(self, ident: str) -> list[tuple[str, str]]:
"""``(kind, key)`` for every reference in *ident*'s content model."""
return list(self._content_refs.get(ident, ()))
|
class_members_transitive
class_members_transitive(ident: str) -> list[SpecRef]
Elements in ident or any subclass, walking memberOf downward.
Source code in src/opm/spec_index.py
| def class_members_transitive(self, ident: str) -> list[SpecRef]:
"""Elements in *ident* or any subclass, walking ``memberOf`` downward."""
out: list[SpecRef] = []
seen: set[str] = set()
stack = [ident]
while stack:
current = stack.pop()
if current in seen:
continue
seen.add(current)
for member in self._members_of.get(current, ()):
spec = self._specs.get(member)
if spec is None:
continue
if spec.kind == 'element':
out.append(self.ref(spec.ident))
elif spec.kind == 'class':
stack.append(spec.ident)
return unique_refs(out)
|
expand_model_ancestors
expand_model_ancestors(keys: list[str]) -> list[str]
Walk up model-class membership (p → model.pLike → …).
Source code in src/opm/spec_index.py
| def expand_model_ancestors(self, keys: list[str]) -> list[str]:
"""Walk *up* model-class membership (``p`` → ``model.pLike`` → …)."""
out: list[str] = []
seen: set[str] = set()
stack = list(keys)
while stack:
key = stack.pop()
if not key or key in seen:
continue
seen.add(key)
out.append(key)
spec = self.get(key)
if spec is None:
continue
stack.extend(spec.model_classes)
return out
|
strip_comments(root: _Element) -> _Element
Remove comments from root, except those inside egXML examples.
Text after a comment lives in the comment's .tail; that must be merged
back before removal, or Guidelines headings like
<head><!--4.1-->Historical Considerations</head> lose their title.
Source code in src/opm/spec_index.py
| def strip_comments(root: etree._Element) -> etree._Element:
"""Remove comments from *root*, except those inside egXML examples.
Text after a comment lives in the comment's ``.tail``; that must be merged
back before removal, or Guidelines headings like
``<head><!--4.1-->Historical Considerations</head>`` lose their title.
"""
for comment in root.xpath('.//comment()[not(ancestor::*[namespace-uri() = $ns])]', ns=EX_NS):
parent = comment.getparent()
if parent is None:
continue
if comment.tail:
prev = comment.getprevious()
if prev is not None:
prev.tail = (prev.tail or '') + comment.tail
else:
parent.text = (parent.text or '') + comment.tail
parent.remove(comment)
return root
|
parse_xml
parse_xml(path: Path | str) -> _Element
Parse path with comments removed outside egXML.
Source code in src/opm/spec_index.py
| def parse_xml(path: Path | str) -> etree._Element:
"""Parse *path* with comments removed outside egXML."""
return strip_comments(etree.parse(str(path), _PARSER).getroot())
|
unique_refs
refs without repeats, character data first, then A–Z.
Source code in src/opm/spec_index.py
| def unique_refs(refs: list[SpecRef]) -> list[SpecRef]:
"""*refs* without repeats, character data first, then A–Z."""
seen: set[tuple[str, str]] = set()
out: list[SpecRef] = []
for ref in refs:
key = (ref.kind, ref.ident)
if key in seen:
continue
seen.add(key)
out.append(ref)
out.sort(key=lambda r: (r.kind != 'text', (r.ident or '').lower()))
return out
|
iter_canonical_specs
iter_canonical_specs(root: _Element)
Yield schema specs, skipping examples embedded in egXML.
Source code in src/opm/spec_index.py
| def iter_canonical_specs(root: etree._Element):
"""Yield schema specs, skipping examples embedded in ``egXML``."""
for tag, kind in SPEC_TAGS.items():
for el in root.iter(qn(tag)):
if _inside_egxml(el):
continue
if el.get('mode') == 'delete':
continue
yield el, kind
|