feat(identity): an STS document's doc-number names its directory and its title the address
K3-19 a. `extract.declared_identity` reads what a NISO-STS document states about itself -- exactly one <std-ident> (<doc-number>, <year>) and exactly one <title-wrap> (<full>, else <main>) -- and returns None for every other row, for XML that is not STS, for an unparseable file, and for a document that states neither. A value stated more than once is not read: an adopted standard carries one <std-ident> per issuing body, and picking one is a guess. `okf build` names a document's directory from its <doc-number> through the id grammar, replacing only the file's stem. A declared name another document in the run also claims falls back to the file name for both, said on stderr: the existing collision gate would refuse both with "rename one", and a name read from inside a document is not one a rename can change. `sources[0].title` becomes <doc-number> + <year>, then the <title-wrap> title, then the file name -- the first that survives the gate and can be written into the flow mapping verbatim. Measured on R761, <full> carries a comma, which ends a flow mapping, so it is never the title there; it is never cleaned up either. `resource` stays the inbox-relative file. Every other row, and every profile without an address, is untouched: the identity is asked for only where `sources` is written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
be169eeca0
commit
ee8d5b5776
6 changed files with 230 additions and 13 deletions
|
|
@ -610,6 +610,13 @@ def _xml_document(data: bytes) -> tuple[str, tuple[OutlineMark, ...]]:
|
|||
document type declaration costs nothing here (0 of 1 file carries one) and
|
||||
holds on every interpreter.
|
||||
"""
|
||||
root = _parse_xml(data)
|
||||
reader = _XmlTextExtractor(sts=_is_sts(root))
|
||||
return reader.text(root), tuple(reader.marks)
|
||||
|
||||
|
||||
def _parse_xml(data: bytes) -> Element:
|
||||
"""The one parse, with the DTD refusal in front of it (see `_xml_document`)."""
|
||||
text = decode_text(data)
|
||||
prologue = text[: text.find("<", text.find("<") + 1) + 1] if "<" in text else text
|
||||
if "<!DOCTYPE" in prologue or "<!DOCTYPE" in text[:4096]:
|
||||
|
|
@ -620,14 +627,81 @@ def _xml_document(data: bytes) -> tuple[str, tuple[OutlineMark, ...]]:
|
|||
code="extractor_xml_doctype",
|
||||
)
|
||||
try:
|
||||
root = ElementTree.fromstring(text)
|
||||
return ElementTree.fromstring(text)
|
||||
except ElementTree.ParseError as exc:
|
||||
raise ExtractionError(
|
||||
f"the XML parser failed on this file: {exc}", code="extractor_xml_parse_error"
|
||||
) from exc
|
||||
sts = _local_name(root.tag) == _STS_ROOT or next(root.iter("sec"), None) is not None
|
||||
reader = _XmlTextExtractor(sts=sts)
|
||||
return reader.text(root), tuple(reader.marks)
|
||||
|
||||
|
||||
def _is_sts(root: Element) -> bool:
|
||||
"""The NAMED schema test: a `<standard>` root, or any `<sec>`."""
|
||||
return _local_name(root.tag) == _STS_ROOT or next(root.iter("sec"), None) is not None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeclaredIdentity:
|
||||
"""What a document states about itself, read from its own elements.
|
||||
|
||||
Each field is `None` when the document does not state it, and ALSO when it
|
||||
states it more than once: an adopted standard carries one `<std-ident>` per
|
||||
body that issued it, and taking the first would be a guess dressed as a
|
||||
reading. The caller falls back to the file name for whatever is `None`.
|
||||
"""
|
||||
|
||||
doc_number: str | None
|
||||
year: str | None
|
||||
title: str | None
|
||||
|
||||
|
||||
def declared_identity(name: str, data: bytes) -> DeclaredIdentity | None:
|
||||
"""`xml`: the identity a NISO-STS document declares, or `None`.
|
||||
|
||||
MEASURED ON THE ONE STS DOCUMENT THIS ROW HAS: exactly one `<std-ident>`
|
||||
(`<doc-number>R761 Prosesskoden</doc-number>` beside `<year>2025</year>`)
|
||||
and one `<title-wrap>` whose `<full>` is the document's title -- while the
|
||||
file carrying it was named for a delivery path, a UUID occurring 0 times in
|
||||
the document. `<doc-type>` is read by nobody: it said `Innledning` there,
|
||||
which is the name of a chapter and not a kind of document.
|
||||
|
||||
`None` for every other row and for XML that is not STS: a declaration is a
|
||||
property of a schema, and a text that merely LOOKS like one declares
|
||||
nothing. An unparseable file is `None` too, never an exception -- extracting
|
||||
the same bytes refuses it with its own code, and an identity is not the
|
||||
place a document is refused.
|
||||
"""
|
||||
if Path(name).suffix.lower() != ".xml":
|
||||
return None
|
||||
try:
|
||||
root = _parse_xml(data)
|
||||
except ExtractionError:
|
||||
return None
|
||||
if not _is_sts(root):
|
||||
return None
|
||||
declared = [
|
||||
(_child_text(element, "doc-number"), _child_text(element, "year"))
|
||||
for element in root.iter()
|
||||
if _local_name(element.tag) == "std-ident"
|
||||
]
|
||||
declared = [pair for pair in declared if pair[0]]
|
||||
doc_number, year = declared[0] if len(declared) == 1 else (None, None)
|
||||
wraps = [element for element in root.iter() if _local_name(element.tag) == "title-wrap"]
|
||||
title = (
|
||||
(_child_text(wraps[0], "full") or _child_text(wraps[0], "main"))
|
||||
if len(wraps) == 1
|
||||
else None
|
||||
)
|
||||
if doc_number is None and title is None:
|
||||
return None
|
||||
return DeclaredIdentity(doc_number=doc_number, year=year, title=title)
|
||||
|
||||
|
||||
def _child_text(element: Element, name: str) -> str | None:
|
||||
"""A direct child's whole text, whitespace collapsed; `None` when absent or empty."""
|
||||
for child in element:
|
||||
if _local_name(child.tag) == name:
|
||||
return " ".join("".join(child.itertext()).split()) or None
|
||||
return None
|
||||
|
||||
|
||||
def _extract_xml(data: bytes) -> str:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue