380160cc2b
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 380160cc2b |
refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit validation idiom at the wrong granularity: no per-constraint removability, no isolable failing/near-miss tests. Split them into 32 named rules, each finding tagged at its origin and cached per XSDContext so the walk still runs once per document (the #101 decision against duplicating XSD's recursive type resolution still holds). Wired the streaming path's ID/IDREF and identity checks through the same framework instead of calling the validators imperatively, and added the unwrapAndLookup combinator the rule already named as required. Also includes the mandated repo-wide swiftformat/swiftlint pass. |
|||
|
0bda990f80
|
perf(serialize): scan bytes, not graphemes, to find characters to escape (#297)
The bulk-copy escape loop still iterated Characters and compared each to '&', '<', '>' (a grapheme comparison per character), the dominant remaining serialize cost for content that needs escaping. Every escaping trigger is an ASCII marker, or in ASCII-only mode a non-ASCII lead byte, so find the next one with value.utf8.firstIndex(where:) and decode no grapheme for the run between markers. The marker byte always lands on a grapheme boundary: an ASCII byte is its own scalar and forces a cluster break (so '&', '<', '>', the quote, and whitespace are never mid-cluster), and after a grapheme-aligned runStart the first byte at or above 0x80 is a lead byte. So value[marker] and value[runStart ..< marker] stay valid, and a multi-scalar cluster escapes as a unit (Swift rounds a mid-grapheme index to its boundary, so the base letter is neither dropped nor duplicated). The output is byte-identical. Measured serialize ~23% faster on the 20k-item generated corpus via a same-state A/B (~2.8x to ~2.2x slower than libxml2). Verified by EscapingBulkCopyTests (the oracle differential extended with combining-mark, ZWJ, and flag clusters), the Apache Xalan gold-output and C14N suites, the full suite, the WASM build, and an adversarial grapheme-alignment review. |
|||
|
1d4ec91922
|
perf(serialize): bulk-copy verbatim runs when escaping output (#296)
Escaping.text and Escaping.attribute rebuilt a value that needed escaping one character at a time: result += String(character) for every unescaped character, plus the escaped form for each marker. Re-escaping a text node that holds a single '&' rebuilt the whole value grapheme by grapheme, and the per-character String construction and append dominated the serialize path for entity-bearing content. Copy each run between escapable characters in a single append and rewrite only the markers. The no-escaping-needed fast path (return the value unchanged) is untouched. The output is byte-identical: matching is per-Character so multibyte graphemes copy whole, and the escaped forms are the same. Measured serialize ~7% faster on the 20k-item generated corpus via a same-state A/B (~3.0x to ~2.8x slower than libxml2). Verified by EscapingBulkCopyTests (a per-character-oracle differential with multibyte cases), the Apache Xalan gold-output and C14N canonicalization suites, the full suite, the WASM build, and an adversarial review that included a 400k-input fuzz. |
|||
|
0abbe348d7
|
perf(emitting): skip escaping rebuild for values that need no escaping (#279)
The serializer's escaping functions rebuilt every text, attribute, comment, and processing-instruction value character by character, even when the value contained nothing to escape, which is the common case. Each now scans the value's UTF-8 bytes for the bytes it would escape (the markup characters, the active attribute quote, the whitespace characters, a comment hyphen, a PI question mark, and any non-ASCII byte in ASCII-only mode) and returns the value unchanged when none is present, taking the original character path only when something must be escaped. Measured serialize roughly halved, ~5.5x to ~2.7x slower than libxml2 on the 20k-item generated corpus, with byte-identical output. EscapingFastPathTests pins both the unchanged fast path and the escaped slow path for each function, and the Apache Xalan XSLT gold-output corpus (a serialize-heavy differential) stays green. No output changed. |
|||
|
f1bc46e99b
|
fix(emit): split ?> in processing-instruction data with a space (#262)
PI data may not contain ?>, which would terminate the instruction early. The serializer emitted it raw, producing malformed output. Insert a space between an offending ? and > (XSLT 1.0 7.3 recovery: a processor recovers from a generated ?> by inserting a space after the ?), via a shared Escaping helper applied at both XML PI serialization sites. A trailing ? is left alone. Closes Apache Xalan conformance case output72. |
|||
|
771fa4b457
|
XSLT/XPath conformance: close 68 Apache Xalan cases (#254)
* fix(xslt): keep namespace nodes on an included literal result element
An xsl:include'd stylesheet's declarations are absorbed after collectUnit
returns, and each literal result element resolves its 7.1.1 namespace nodes
by walking the weak parent chain back to its own included stylesheet element.
Those elements lived only in the write-only retainedRoots array, so an
optimized build released them before the deferred resolution ran and the
in-scope xmlns declarations vanished, dropping the namespace nodes the
included element should carry. The output was correct in debug and wrong in
release. Pin the retained roots across the resolution with
withExtendedLifetime. Imports were unaffected because they compile eagerly
while their tree is still a live local.
Catches Apache Xalan conformance case lre/lre05, surfaced by running the
apache/xalan-test corpus.
* fix(xslt): select a called template by import precedence, not document order
xsl:call-template looked up the target with the first match by position in the
flattened template list. Imports fold in before the including unit's own and
included templates, so when an import and an include both define a template
with the same name, the imported one was wrongly chosen. An included template
carries the including unit's higher import precedence and must win (XSLT 1.0
section 6). The lookup now picks the highest-precedence definition among the
same-name templates.
Closes Apache Xalan conformance cases namedtemplate17, namedtemplate18,
namedtemplate19, and namespace13 (removed from the xalan-test baseline). Full
suite 1608 tests green in debug and release; the Xalan suite passes.
* fix(xpath): string-value of a comment or PI node is its own data
The XPath string-value of a comment node is its character data and of a
processing-instruction node is its data (XPath 1.0 5.6, 5.7), but TreeNode
returned the empty string for both, so value-of select="comment()", and any
matching, sorting, or comparison over comment/PI nodes, produced nothing.
stringValue was conflated with the text-only concatenation that forms an
element's or root's string-value, where comment and PI descendants correctly
contribute nothing (5.1, 5.5). That concatenation now uses a separate
textContent, so an element's string-value still excludes comment and PI text
while a comment or PI node reports its own data.
Closes 24 Apache Xalan conformance cases across node, axes, position, select,
modes, conflictres, and variable (removed from the baseline). Full suite 1609
tests green in debug and release; the Xalan suite passes.
* fix(xslt): resolve top-level variables in any order, not document order
Top-level variables and parameters may reference one another regardless of
declaration order (XSLT 1.0 section 11.4), but they were evaluated once
top-to-bottom, so a forward reference (a global declared before the one it
uses) saw an undefined value and resolved to empty. Globals now evaluate to a
fixpoint: each pass re-evaluates the unsettled ones with the latest bindings,
so a dependency chain in any order settles in at most N passes, a pass that
changes nothing stops early, and a circular reference stops at the cap.
Extracted the evaluation into Transformer.evaluatedGlobals (XSLTGlobals.swift).
Closes Apache Xalan conformance cases variable33, variable34, variable35. Full
suite 1611 tests green in debug and release; the Xalan suite passes.
* fix(xslt): an empty-content variable with no select is the empty string
XSLT 1.0 section 11.2 makes <xsl:variable name="x"/> (empty content, no select)
equivalent to select="", so boolean() of it is false. It was always built as a
result-tree-fragment document node, which boolean() reports as true. A variable
with no select and empty content now yields the empty string; only non-empty
content remains a result tree fragment.
Closes Apache Xalan conformance cases boolean43 and boolean87. Full suite 1612
tests green in debug and release; the Xalan suite passes.
* fix(xpath): allow whitespace around the :: axis separator
XPath 1.0 allows whitespace between tokens, so `child :: sub` is the child axis
with node test `sub`. The parser only matched `::` immediately after the axis
name, so a spaced separator fell back to the child axis with the axis name read
as an element test, selecting nothing. parseAxis now skips whitespace after the
axis name and after `::`; on no match the reset to before the name is unchanged,
so a bare name step is unaffected.
Closes Apache Xalan conformance cases select16, select27, select28. Full suite
1613 tests green in debug and release; the Xalan suite passes.
* feat(xslt): document('') returns the stylesheet's own document
XSLT 1.0 section 12.1 makes an empty URI reference the document containing the
expression, so a stylesheet can read its own xsl:* nodes (embedded lookup
tables, self-inspection). The reference went straight to the document loader,
which has no entry for it, so document('') returned an empty node-set. The
transformer now keeps its parsed stylesheet tree and the document function
returns it for an empty reference, cached like any loaded document; a #fragment
still applies.
Closes Apache Xalan conformance cases select67, select68, idkey50, mdocs17,
namespace20. Full suite 1614 tests green in debug and release; Xalan passes.
* fix(xslt): implement unparsed-entity-uri from the source DTD
unparsed-entity-uri was a stub returning the empty string. It now reads the
source document type's unparsed (NDATA) entities, threaded to the function table
alongside the document loader, and returns the named entity's system URI; an
unknown name still returns empty.
Closes Apache Xalan conformance case expression02. Full suite 1615 tests green
in debug and release; the Xalan suite passes.
* fix(xpath): substring-after(s, '') returns s
Per the XPath 1.0 errata, an empty second argument matches at the start of the
string, so substring-after returns the whole string. The function bailed to the
empty string on an empty separator. substring-before(s, '') stays empty, as the
spec requires.
Closes Apache Xalan conformance case string143. Full suite 1615 tests green in
debug and release; the Xalan suite passes.
* fix(xslt): match a template mode by expanded name, not its prefix
XSLT 1.0 section 5.7 compares a mode by expanded name, so two prefixes bound to
the same namespace name the same mode. The prefixed QName string was stored and
compared verbatim, so apply-templates mode="foo:m" missed a template with
mode="moo:m" even when foo and moo share a namespace. Mode names on xsl:template
and xsl:apply-templates now resolve to {uri}local at parse time.
Moved the parser's declaration helpers to XSLTParserDeclarations.swift to keep
the file within the length budget.
Closes Apache Xalan conformance case modes16. Full suite 1616 tests green in
debug and release; the Xalan suite passes.
* fix(xslt): name a variable by expanded QName, not its prefix
XSLT 1.0 section 11.1 names a variable by expanded name, so two prefixes bound
to the same namespace name the same variable. The declared name and the
$prefix:local reference were compared as raw strings, so $new:x did not see a
variable declared txt:x even when new and txt share a namespace. A prefixed
declared name now resolves to {uri}local at parse time, and the XPath evaluator
falls back to the namespace-resolved name when the raw key is unbound, so a
caller-supplied raw binding still works and unprefixed names are unchanged.
Closes Apache Xalan conformance case variable55. Full suite 1617 tests green in
debug and release; the Xalan suite passes.
* fix(xslt): a match pattern predicate sees the global variables
XSLT 1.0 section 5.2 evaluates a pattern predicate with the top-level (global)
variables in scope, but pattern matching ran with no variables bound, so a
template like match="foo[. > $screen]" never matched ($screen was undefined).
The transformer supplies the global bindings to the match cache; they are
constant per transform, so they do not enter the cache key.
Closes Apache Xalan conformance cases match14 and idkey43. Full suite 1618 tests
green in debug and release; the Xalan suite passes.
* fix(emit): make a comment ending in a hyphen or containing -- well-formed
XML forbids `--` inside a comment and a `-` immediately before `-->`, so a
comment whose text ends with `-` was emitted as <!--text---> (malformed). The
serializer now inserts a space after each offending hyphen (the XSLT 1.0 16
recovery), so `text-` becomes `text- `. Applied in both the Serializer and the
streaming Writer via a shared Escaping.comment helper.
Closes Apache Xalan conformance cases output89 and output90. Full suite 1619
tests green in debug and release; the Xalan suite passes.
* fix(xslt): preserve whitespace adjacent to a CDATA section in a template
Text and CDATA are one text node in the data model, so <out> <![CDATA[test]]>
</out> is the node " test ", whose surrounding spaces must survive whitespace
stripping (XSLT 1.0 3.4). The template body dropped each whitespace-only text
node independently, so the spaces around the CDATA vanished. Adjacent text and
CDATA children now coalesce into one run, dropped only when the whole run is
whitespace; a purely whitespace run between elements is still removed.
Closes Apache Xalan conformance case whitespace13. Full suite 1620 tests green
in debug and release; the Xalan suite passes.
* fix(xslt): xsl:processing-instruction/comment keep only text-node content
XSLT 1.0 sections 7.4 and 7.6 make it an error for xsl:comment or
xsl:processing-instruction content to create a non-text node; the recovery is
to ignore the offending node together with its content. PureXML took the
string-value of all instantiated content, so a copied element leaked its text
into the PI or comment. The content now contributes only its text and CDATA
node items.
Closes Apache Xalan conformance case copy60. Full suite 1621 tests green in
debug and release; the Xalan suite passes.
* fix(xslt): xsl:attribute keeps only text-node content (errata E27)
XSLT 1.0 errata E27 makes it an error for xsl:attribute content to create a
non-text node, recovering by ignoring the offending node together with its
content (the same rule as xsl:comment/processing-instruction). PureXML took the
string-value of all instantiated content, so a copied element leaked its text
into the attribute value. The content now contributes only its text and CDATA
node items, reusing textNodesOnly.
Closes Apache Xalan conformance cases copy56 and copy58. Full suite 1622 tests
green in debug and release; the Xalan suite passes.
* fix(xslt): xsl:copy-of carries an element's inherited namespace nodes
XSLT 1.0 11.3 copies a node together with its namespace nodes, so copying an
element must reproduce its full in-scope set, including a declaration inherited
from an ancestor. copy-of reproduced only the element's own xmlns attributes,
dropping inherited ones. The inherited declarations are now added to the copied
element and the serializer's namespace fixup drops any already in the result
scope.
Closes Apache Xalan conformance cases copy09, copy27, copy48, mdocs09. Full
suite 1623 tests green in debug and release; the Xalan suite passes.
* fix(xslt): xsl:element with an unusable name passes its content through
XSLT 1.0 7.1.2 makes a name that is not a QName an error; the recovery (the
pinned element-name-not-QName "pass-through") emits the element's content
without the wrapper. The check rejected only an empty or multi-colon name, so
an undeclared prefix (none:foo) or a name that is not an NCName (this is a
string) still produced a malformed element. The name is now also rejected when
a part is not an NCName or its prefix is unbound.
Closes Apache Xalan conformance cases namespace40, namespace42, namespace43,
namespace60, namespace73, namespace79. Full suite 1624 tests green in debug and
release; the Xalan suite passes.
* fix(xslt): xsl:copy of the root node honors use-attribute-sets
Copying the root node produces no element of its own, so the use-attribute-sets
attributes have nowhere to attach on the copy and instead join the enclosing
result element (XSLT 1.0 7.5). copyInstruction dropped them for the root case;
they are now emitted ahead of the copied content so the enclosing element picks
them up.
Closes Apache Xalan conformance cases attribset27 and attribset29. Full suite
1625 tests green in debug and release; the Xalan suite passes.
* refactor(xslt): address review (canonical name/whitespace checks, html comment safety)
Review follow-ups on the conformance series:
- xsl:element name validation now uses the canonical Parsing.XMLCharacter
.isValidName instead of a hand-rolled grapheme check, which had narrowed valid
XML names (it dropped a name using a non-ASCII NameChar such as U+00B7).
- The template-body whitespace check reuses Parsing.XMLCharacter.isWhitespace
instead of a third local copy of the same predicate.
- The HTML serializer applies the section-16 comment recovery too, so a
method="html" comment ending in a hyphen (or containing two) is well-formed;
the C14N canonicalizer keeps comments verbatim as its contract requires.
Adds regression tests for a non-ASCII NameChar element name and for html
comment hyphen-safety. Full suite 1626 tests green in debug and release; the
Xalan suite passes.
|
|||
| 87fd7ba889 | feat(emitting): textEscaping round-trip option to preserve carriage returns (#91) | |||
| e2ba6a7359 |
feat(emitting): cdataAsText and asciiOnly save options
- cdataAsText emits a CDATA section's content as escaped text instead of a <![CDATA[ ... ]]> section. - asciiOnly escapes every non-ASCII character as a numeric character reference, so the output is pure ASCII (libxml2's character-reference encoding). Both apply to the Serializer and the incremental Writer through the shared Escaping rules. Closes #91 |
|||
| cd2f30a262 |
feat(emitting): attribute quote style, line endings, xml:space, writer namespaces
Broaden the serializer and writer options from the audit: - attributeQuote selects single or double quotes; the active quote is escaped in values (apostrophe as ' in single-quote mode). - lineEnding controls the inserted newline (set to CRLF for \r\n output). - xml:space=preserve suppresses pretty-print indentation for an element and its subtree; xml:space=default re-enables it. - Writer gains writeStartElementNS(prefix:localName:namespaceURI:) and writeNamespace(prefix:uri:), so callers no longer hand-format prefixes and xmlns attributes. CDATA-vs-escape and entity re-escaping strategies are left as a follow-up; the current escaping is the safe default. Closes #91 |
|||
| 9b55767b39 |
feat(emitting): add an incremental XML writer (xmlTextWriter)
Add PureXML.Emitting.Writer, the libxml2 xmlTextWriter model: emit a document with writeStartElement/writeAttribute/writeString/writeEndElement and friends, never building a tree. Escaping is extracted into a shared Emitting.Escaping helper used by both the writer and the serializer, so compact writer output is byte-identical to serializing the equivalent tree. Pretty mode indents element children (forward-only, so prefer compact for mixed content). Closes #32. Reimplemented from the libxml2 model; no upstream code copied. 6 tests; 90 total. |