feat(xslt): key() over attribute nodes, key/id match patterns, doctyped sources
key() and document() read the string value of attribute and namespace
node arguments instead of dropping them, so the Muenchian grouping
idiom works; match patterns evaluate with the XSLT function table,
making key('k','v') and id('x') patterns match; and the XSLT entry
points parse sources, stylesheets, and document() loads with doctypes
allowed, as the conformance corpora require. Xalan baseline
292 -> 263 (idkey 39 -> 16).
This commit is contained in:
parent
b41678064e
commit
da8bb6b92a
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Keys, ids, and doctyped sources, seventh xalan burn-down (#130): `key()` and `document()` read the string value of attribute and namespace node arguments instead of dropping them (the Muenchian grouping idiom works); match patterns evaluate with the XSLT function table, so `key('k', 'v')` and `id('x')` patterns match (a key/id pattern branch is also no longer prefixed with `//`); and the XSLT entry points parse sources, stylesheets, and `document()` loads with doctypes allowed, which the conformance corpora use throughout. Baseline 292 to 263 (idkey 39 to 16, the rest spread).
|
||||
- Templates over attribute and namespace nodes, sixth xalan burn-down (#130): xsl:apply-templates and xsl:for-each now keep attribute and namespace nodes in their selections instead of dropping them at the tree-node conversion. The evaluation context carries the non-tree current node (its owner element backs the tree-only machinery), XPath evaluation and current() start from it, match patterns select attribute nodes (the match cache keeps attribute identities, owner plus name, alongside tree identities), the built-in rules copy an attribute's or namespace's value as text, xsl:copy copies the focused attribute or namespace declaration itself, and an attribute result arriving after element content is ignored (the XSLT recovery). Sorting and apply-imports work over the generalized node kind. Baseline 352 to 292 (axes 35 to 16, copy 29 to 16, attribset 18 to 13, the rest spread).
|
||||
- Output methods in depth, fifth xalan burn-down (#130): `disable-output-escaping` on xsl:value-of and xsl:text (raw text travels bracketed by private-use sentinels and the serializer's escaping is undone after the fact; a string extracted from a result tree fragment loses the markers per 16.4, so the variable round-trip case re-escapes); the default output method is html when the first result element is named html with a null namespace (16.1); the xml method writes the XML declaration unless `omit-xml-declaration="yes"` (the spec default, with the caller's emit options as fallback only on explicit stylesheet silence); the html method injects `META http-equiv="Content-Type"` into head (16.2), keeps SCRIPT/STYLE raw and void elements end-tag-free case-insensitively, and escapes U+00A0-U+00FF to the HTML 4.01 Latin-1 entity names. Whitespace-only detection in stylesheet/source stripping now means XML whitespace only, so an NBSP-only text node survives. The conf comparison treats whitespace adjacent to tag boundaries as layout for non-canonical comparisons. Baseline 371 to 352 (output 49 to 30).
|
||||
- Literal result element namespace copying, fourth xalan burn-down (#130): a literal result element now copies its in-scope namespace declarations to the result per 7.1.1, minus the XSLT namespace and the namespaces of the prefixes listed in `exclude-result-prefixes`/`extension-element-prefixes` in scope (`#default` included); an aliased stylesheet namespace declares its result prefix and URI instead. The namespace-fixup pass drops any declaration that repeats an inherited binding, so the copied nodes do not redeclare on every descendant, and created names now reuse the stylesheet's own prefixes (`anamespace:Attr1` rather than `ns0:Attr1`). `xsl:output indent="yes"` switches to the corpus golds' Xalan indentation form: children on their own lines with no leading spaces. Two unit expectations updated to the spec behavior (a literal element under stylesheet-level declarations carries them; collapsed alias+result declarations). The xalan baseline drops from 446 to 371 (namespace 89 to 42, output 50 to 49 and others spread across categories).
|
||||
|
||||
@ -200,7 +200,7 @@ extension PureXML.XSLT {
|
||||
// A node-set second argument unions the matches for each node's
|
||||
// string value; any other value is used directly as a string.
|
||||
let values: [String] = if let nodes = arguments[1].nodes {
|
||||
nodes.compactMap(\.treeNode).map(\.stringValue)
|
||||
nodes.map(\.stringValue)
|
||||
} else {
|
||||
[arguments[1].string]
|
||||
}
|
||||
@ -216,7 +216,7 @@ extension PureXML.XSLT {
|
||||
// A string or a node-set of URI references, each optionally with
|
||||
// a `#fragment` selecting a subset of the loaded document.
|
||||
let references: [String] = if let nodes = arguments.first?.nodes {
|
||||
nodes.compactMap(\.treeNode).map(\.stringValue)
|
||||
nodes.map(\.stringValue)
|
||||
} else {
|
||||
[arguments.first?.string ?? ""]
|
||||
}
|
||||
@ -252,7 +252,7 @@ extension PureXML.XSLT {
|
||||
fragment = nil
|
||||
}
|
||||
if cache.trees[path] == nil {
|
||||
guard let text = loader(path), let parsed = try? PureXML.parse(text) else { return [] }
|
||||
guard let text = loader(path), let parsed = try? PureXML.parse(text, limits: .init(allowDoctype: true)) else { return [] }
|
||||
cache.sources[path] = parsed
|
||||
cache.trees[path] = PureXML.Model.TreeNode(parsed)
|
||||
}
|
||||
|
||||
@ -19,25 +19,26 @@ extension PureXML.XSLT {
|
||||
private var matched: [String: PatternMatches] = [:]
|
||||
|
||||
/// The identities of the tree nodes `pattern` selects over `root`.
|
||||
func nodes(matching pattern: String, over root: PureXML.Model.TreeNode) -> Set<ObjectIdentifier> {
|
||||
compute(pattern, root).trees
|
||||
func nodes(matching pattern: String, over root: PureXML.Model.TreeNode, functions: PureXML.XPath.FunctionTable = .init()) -> Set<ObjectIdentifier> {
|
||||
compute(pattern, root, functions).trees
|
||||
}
|
||||
|
||||
/// The identities of the attribute nodes `pattern` selects over `root`.
|
||||
func attributes(matching pattern: String, over root: PureXML.Model.TreeNode) -> Set<AttributeIdentity> {
|
||||
compute(pattern, root).attributes
|
||||
func attributes(matching pattern: String, over root: PureXML.Model.TreeNode, functions: PureXML.XPath.FunctionTable = .init()) -> Set<AttributeIdentity> {
|
||||
compute(pattern, root, functions).attributes
|
||||
}
|
||||
|
||||
/// Computed and cached on first use; a branch that fails to compile
|
||||
/// selects nothing.
|
||||
private func compute(_ pattern: String, _ root: PureXML.Model.TreeNode) -> PatternMatches {
|
||||
/// selects nothing. The function table makes `key()` and `id()`
|
||||
/// patterns work.
|
||||
private func compute(_ pattern: String, _ root: PureXML.Model.TreeNode, _ functions: PureXML.XPath.FunctionTable) -> PatternMatches {
|
||||
if let cached = matched[pattern] { return cached }
|
||||
var result = PatternMatches()
|
||||
for branch in pattern.split(separator: "|") {
|
||||
let trimmed = branch.trimmingXMLWhitespace()
|
||||
let path = trimmed.hasPrefix("/") ? trimmed : "//" + trimmed
|
||||
let path = trimmed.hasPrefix("/") || trimmed.hasPrefix("key(") || trimmed.hasPrefix("id(") ? trimmed : "//" + trimmed
|
||||
guard let query = try? PureXML.XPath.Query(path),
|
||||
let value = try? query.value(atNode: .tree(root), position: 1, size: 1, variables: [:], functions: PureXML.XPath.FunctionTable()),
|
||||
let value = try? query.value(atNode: .tree(root), position: 1, size: 1, variables: [:], functions: functions),
|
||||
let selected = value.nodes
|
||||
else { continue }
|
||||
for node in selected {
|
||||
|
||||
@ -92,7 +92,7 @@ extension PureXML.XSLT {
|
||||
/// are literal result elements.
|
||||
enum XSLTParser {
|
||||
static func parse(_ xsl: String, loader: (String) -> String? = { _ in nil }) throws -> Stylesheet {
|
||||
let root = try PureXML.parseTree(xsl)
|
||||
let root = try PureXML.parseTree(xsl, limits: .init(allowDoctype: true))
|
||||
guard let top = stylesheetElement(root) else { throw XSLTError.notAStylesheet }
|
||||
return compile(top, loader: loader, precedence: 0)
|
||||
}
|
||||
@ -150,7 +150,7 @@ extension PureXML.XSLT {
|
||||
|
||||
private static func load(_ node: XSLTTree, loader: (String) -> String?, precedence: Int) -> Stylesheet? {
|
||||
guard let href = XSLTNode.attribute(node, "href"), let text = loader(href),
|
||||
let root = try? PureXML.parseTree(text), let top = stylesheetElement(root)
|
||||
let root = try? PureXML.parseTree(text, limits: .init(allowDoctype: true)), let top = stylesheetElement(root)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ public extension PureXML.XSLT {
|
||||
) throws -> String {
|
||||
let documentLoader = resolvingLoader(documentLoader, baseURI: baseURI)
|
||||
let sheet = try XSLTParser.parse(stylesheet, loader: documentLoader)
|
||||
let root = try PureXML.parseTree(source)
|
||||
let root = try PureXML.parseTree(source, limits: .init(allowDoctype: true))
|
||||
Whitespace.strip(root, stylesheet: sheet)
|
||||
let transformer = Transformer(stylesheet: sheet, root: root, documentLoader: documentLoader)
|
||||
let result = transformer.run()
|
||||
@ -137,7 +137,7 @@ public extension PureXML.XSLT {
|
||||
) throws -> PureXML.Model.Node {
|
||||
let documentLoader = resolvingLoader(documentLoader, baseURI: baseURI)
|
||||
let sheet = try XSLTParser.parse(stylesheet, loader: documentLoader)
|
||||
let root = try PureXML.parseTree(source)
|
||||
let root = try PureXML.parseTree(source, limits: .init(allowDoctype: true))
|
||||
Whitespace.strip(root, stylesheet: sheet)
|
||||
let transformer = Transformer(stylesheet: sheet, root: root, documentLoader: documentLoader)
|
||||
let result = transformer.run()
|
||||
|
||||
@ -54,7 +54,7 @@ extension PureXML.XSLT {
|
||||
}
|
||||
|
||||
func matches(_ node: PureXML.Model.TreeNode, _ pattern: String) -> Bool {
|
||||
matchCache.nodes(matching: pattern, over: root).contains(ObjectIdentifier(node))
|
||||
matchCache.nodes(matching: pattern, over: root, functions: patternFunctions()).contains(ObjectIdentifier(node))
|
||||
}
|
||||
|
||||
/// Pattern membership for any XPath node kind.
|
||||
@ -63,13 +63,25 @@ extension PureXML.XSLT {
|
||||
case let .tree(tree):
|
||||
matches(tree, pattern)
|
||||
case let .attribute(owner, attribute):
|
||||
matchCache.attributes(matching: pattern, over: root)
|
||||
matchCache.attributes(matching: pattern, over: root, functions: patternFunctions())
|
||||
.contains(PureXML.XSLT.AttributeIdentity(owner: ObjectIdentifier(owner), name: attribute.name.description))
|
||||
case .namespace:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The function table match patterns evaluate with (`key()`/`id()`
|
||||
/// patterns), rooted at the document.
|
||||
private func patternFunctions() -> PureXML.XPath.FunctionTable {
|
||||
PureXML.XSLT.Library.table(
|
||||
current: .tree(root),
|
||||
keys: keyIndex,
|
||||
loader: documentLoader,
|
||||
decimalFormats: stylesheet.decimalFormats,
|
||||
documents: documentCache,
|
||||
)
|
||||
}
|
||||
|
||||
fileprivate func applyTemplates(
|
||||
to nodes: [PureXML.XPath.Node],
|
||||
mode: String?,
|
||||
|
||||
@ -42,7 +42,6 @@ conflictres/conflictres30
|
||||
conflictres/conflictres34
|
||||
conflictres/conflictres37
|
||||
copy/copy09
|
||||
copy/copy16
|
||||
copy/copy19
|
||||
copy/copy20
|
||||
copy/copy21
|
||||
@ -62,49 +61,25 @@ expression/expression02
|
||||
extend/extend01
|
||||
extend/extend02
|
||||
extend/extend04
|
||||
idkey/idkey01
|
||||
idkey/idkey03
|
||||
idkey/idkey04
|
||||
idkey/idkey09
|
||||
idkey/idkey12
|
||||
idkey/idkey18
|
||||
idkey/idkey21
|
||||
idkey/idkey22
|
||||
idkey/idkey23
|
||||
idkey/idkey24
|
||||
idkey/idkey26
|
||||
idkey/idkey30
|
||||
idkey/idkey31
|
||||
idkey/idkey32
|
||||
idkey/idkey33
|
||||
idkey/idkey35
|
||||
idkey/idkey37
|
||||
idkey/idkey38
|
||||
idkey/idkey39
|
||||
idkey/idkey40
|
||||
idkey/idkey41
|
||||
idkey/idkey42
|
||||
idkey/idkey43
|
||||
idkey/idkey44
|
||||
idkey/idkey45
|
||||
idkey/idkey46
|
||||
idkey/idkey47
|
||||
idkey/idkey48
|
||||
idkey/idkey50
|
||||
idkey/idkey53
|
||||
idkey/idkey55
|
||||
idkey/idkey56
|
||||
idkey/idkey57
|
||||
idkey/idkey58
|
||||
idkey/idkey59
|
||||
idkey/idkey60
|
||||
idkey/idkey61
|
||||
idkey/idkey62
|
||||
idkey/idkey63
|
||||
impincl/impincl07
|
||||
impincl/impincl08
|
||||
impincl/impincl11
|
||||
impincl/impincl17
|
||||
impincl/impincl19
|
||||
impincl/impincl21
|
||||
impincl/impincl23
|
||||
@ -112,7 +87,6 @@ impincl/impincl25
|
||||
impincl/impincl26
|
||||
impincl/impincl27
|
||||
lre/lre12
|
||||
match/match11
|
||||
match/match14
|
||||
mdocs/mdocs01
|
||||
mdocs/mdocs05
|
||||
@ -188,8 +162,6 @@ node/node13
|
||||
node/node15
|
||||
numbering/numbering63
|
||||
numbering/numbering87
|
||||
numbering/numbering90
|
||||
numbering/numbering91
|
||||
numbering/numbering92
|
||||
numbering/numbering93
|
||||
output/output13
|
||||
@ -249,7 +221,6 @@ select/select28
|
||||
select/select59
|
||||
select/select67
|
||||
select/select68
|
||||
select/select73
|
||||
select/select75
|
||||
sort/sort-alphabet-english
|
||||
sort/sort-alphabet-polish
|
||||
|
||||
Loading…
Reference in New Issue
Block a user