diff --git a/CHANGELOG.md b/CHANGELOG.md index f042a90..5f2cd44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/Sources/XSLT/XSLTFunctions.swift b/Sources/XSLT/XSLTFunctions.swift index ee0dda0..7bed319 100644 --- a/Sources/XSLT/XSLTFunctions.swift +++ b/Sources/XSLT/XSLTFunctions.swift @@ -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) } diff --git a/Sources/XSLT/XSLTMatchCache.swift b/Sources/XSLT/XSLTMatchCache.swift index 0f0b161..eefd09b 100644 --- a/Sources/XSLT/XSLTMatchCache.swift +++ b/Sources/XSLT/XSLTMatchCache.swift @@ -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 { - compute(pattern, root).trees + func nodes(matching pattern: String, over root: PureXML.Model.TreeNode, functions: PureXML.XPath.FunctionTable = .init()) -> Set { + 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 { - compute(pattern, root).attributes + func attributes(matching pattern: String, over root: PureXML.Model.TreeNode, functions: PureXML.XPath.FunctionTable = .init()) -> Set { + 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 { diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift index 7805e04..99c10f1 100644 --- a/Sources/XSLT/XSLTParser.swift +++ b/Sources/XSLT/XSLTParser.swift @@ -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 } diff --git a/Sources/XSLT/XSLTTransform.swift b/Sources/XSLT/XSLTTransform.swift index 469c298..b78503d 100644 --- a/Sources/XSLT/XSLTTransform.swift +++ b/Sources/XSLT/XSLTTransform.swift @@ -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() diff --git a/Sources/XSLT/XSLTTransformer.swift b/Sources/XSLT/XSLTTransformer.swift index ac12378..cf5abb0 100644 --- a/Sources/XSLT/XSLTTransformer.swift +++ b/Sources/XSLT/XSLTTransformer.swift @@ -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?, diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt index cde2612..03470cd 100644 --- a/Tests/Fixtures/xalan-baseline.txt +++ b/Tests/Fixtures/xalan-baseline.txt @@ -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