feat(xslt): per-document key indexes in document order
key() resolves its index against the current node's own document, built lazily per document root, so keys work inside document() trees; a node-set use expression indexes the matched node under every member's string value; and key() results return in document order regardless of lookup value order. Xalan baseline 187 -> 182.
This commit is contained in:
parent
40605ff6e5
commit
7b4887b7ad
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Keys per document, in document order, eleventh xalan burn-down (#130): `key()` resolves its index against the current node's own document (built lazily per document root, so keys work inside document() trees), a node-set `use` expression indexes the matched node under every member's string value, and key() results return in document order regardless of the lookup value order. Baseline 187 to 182.
|
||||
- Sort keys in context and cross-document template matching, tenth xalan burn-down (#130): xsl:sort keys are computed once per node with the node's own evaluation context (its position and size in the selection, the caller's variables, functions, and namespace bindings), so position()-based and variable-referencing keys work, and NaN keys order before every number under data-type="number" (the Xalan order); match patterns evaluate over the matched node's own document rather than the transform source, so templates match nodes loaded through document() (the match cache keys include the document identity). Baseline 204 to 187 (sort 18 to 11, mdocs 14 to 6).
|
||||
- Prolog and CDATA conformance, ninth xalan burn-down (#130): the XML declaration precedes the document type declaration; a public-only doctype is emitted by the html method (16.2) and ignored by the xml method, which requires `doctype-system` (16.1); the simplified stylesheet syntax of 2.3 (a literal result element carrying `xsl:version`) compiles as a single match="/" template; and cdata-section-elements text containing "]]>" splits across section boundaries so no section holds the terminator. Baseline 218 to 204 (output 34 to 20).
|
||||
- Stylesheet namespace resolution, eighth xalan burn-down (#130): a template carries the xmlns bindings in scope at its stylesheet element, and they flow into its match pattern (the match cache keys on pattern plus bindings) and every expression evaluated while it is instantiated, so `match="baz2:doc"` and `select="local-name(baz2:b)"` resolve the prefix against the stylesheet and match by namespace URI; with bindings supplied the XPath 1.0 rule applies exactly, an unprefixed name test selecting only the null namespace. The instruction-scope namespace capture includes the default declaration, so `xsl:element` honors an in-scope `xmlns="uri"` for its unprefixed created names, an in-scope `xmlns=""` keeps them namespace-free (undeclared in the result when an inherited default would capture them), and an unusable created name (`:foo`) drops the wrapper and keeps the content, the Xalan recovery. Baseline 263 to 218 (namespace 41 to 13, axes 16, sort 18, output unchanged at 34, the rest spread).
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
extension PureXML.XSLT {
|
||||
/// The key indexes a transform has built, one per document root.
|
||||
final class KeyIndexCache {
|
||||
var indexes: [ObjectIdentifier: KeyIndex] = [:]
|
||||
}
|
||||
|
||||
/// The documents a transform has loaded, keyed by URI: per the XSLT
|
||||
/// `document()` definition the same reference returns the identical
|
||||
/// document, so node identity (and `generate-id()`) is stable across
|
||||
|
||||
@ -179,7 +179,7 @@ extension PureXML.XSLT {
|
||||
/// reading the prebuilt `keys` index and the injected `document` loader.
|
||||
static func table(
|
||||
current: PureXML.XPath.Node,
|
||||
keys: KeyIndex,
|
||||
keys: @escaping (PureXML.Model.TreeNode) -> KeyIndex,
|
||||
loader: @escaping (String) -> String?,
|
||||
decimalFormats: [String: PureXML.XSLT.DecimalFormat] = [:],
|
||||
documents: PureXML.XSLT.DocumentCache,
|
||||
@ -194,23 +194,8 @@ extension PureXML.XSLT {
|
||||
decimalFormats[name] ?? PureXML.XSLT.DecimalFormat(),
|
||||
))
|
||||
}
|
||||
.adding("key") { arguments, _ in
|
||||
let name = arguments.first?.string ?? ""
|
||||
guard arguments.count > 1 else { return .nodeSet([]) }
|
||||
// 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.map(\.stringValue)
|
||||
} else {
|
||||
[arguments[1].string]
|
||||
}
|
||||
var matched: [PureXML.Model.TreeNode] = []
|
||||
for value in values {
|
||||
for node in keys[name]?[value] ?? [] where !matched.contains(where: { $0 === node }) {
|
||||
matched.append(node)
|
||||
}
|
||||
}
|
||||
return .nodeSet(matched.map { .tree($0) })
|
||||
.adding("key") { arguments, context in
|
||||
keyLookup(arguments, context, keys)
|
||||
}
|
||||
.adding("document") { arguments, _ in
|
||||
// A string or a node-set of URI references, each optionally with
|
||||
@ -235,6 +220,39 @@ extension PureXML.XSLT {
|
||||
.adding("unparsed-entity-uri") { _, _ in .string("") }
|
||||
}
|
||||
|
||||
/// The `key()` implementation: the index belongs to the current
|
||||
/// node's document; a node-set second argument unions the matches for
|
||||
/// each member's string value; results come back in document order.
|
||||
private static func keyLookup(
|
||||
_ arguments: [PureXML.XPath.Value],
|
||||
_ context: PureXML.XPath.EvaluationContext,
|
||||
_ keys: (PureXML.Model.TreeNode) -> KeyIndex,
|
||||
) -> PureXML.XPath.Value {
|
||||
let name = arguments.first?.string ?? ""
|
||||
guard arguments.count > 1 else { return .nodeSet([]) }
|
||||
let values: [String] = if let nodes = arguments[1].nodes {
|
||||
nodes.map(\.stringValue)
|
||||
} else {
|
||||
[arguments[1].string]
|
||||
}
|
||||
let owner: PureXML.Model.TreeNode? = switch context.node {
|
||||
case let .tree(tree): tree
|
||||
case let .attribute(treeOwner, _), let .namespace(treeOwner, _, _): treeOwner
|
||||
}
|
||||
guard var documentRoot = owner else { return .nodeSet([]) }
|
||||
while let parent = documentRoot.parent {
|
||||
documentRoot = parent
|
||||
}
|
||||
let index = keys(documentRoot)
|
||||
var matched: [PureXML.Model.TreeNode] = []
|
||||
for value in values {
|
||||
for node in index[name]?[value] ?? [] where !matched.contains(where: { $0 === node }) {
|
||||
matched.append(node)
|
||||
}
|
||||
}
|
||||
return .nodeSet(matched.map { PureXML.XPath.Node.tree($0) }.sorted(by: PureXML.XPath.Node.precedes))
|
||||
}
|
||||
|
||||
/// Loads one `document()` reference: the whole document, or, when the
|
||||
/// reference has a `#fragment`, the nodes its XPointer selects.
|
||||
private static func documentReference(
|
||||
@ -307,8 +325,17 @@ extension PureXML.XSLT {
|
||||
guard let matchQuery = try? PureXML.XPath.Query(path),
|
||||
let useQuery = try? PureXML.XPath.Query(key.use) else { continue }
|
||||
for node in matchQuery.nodes(over: root) {
|
||||
let value = (try? useQuery.value(at: node).string) ?? ""
|
||||
index[key.name, default: [:]][value, default: []].append(node)
|
||||
// A node-set use expression indexes the node under every
|
||||
// member's string value.
|
||||
let used = try? useQuery.value(at: node)
|
||||
let values: [String] = if let nodes = used?.nodes {
|
||||
nodes.map(\.stringValue)
|
||||
} else {
|
||||
[used?.string ?? ""]
|
||||
}
|
||||
for value in values {
|
||||
index[key.name, default: [:]][value, default: []].append(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
return index
|
||||
|
||||
@ -84,4 +84,22 @@ extension PureXML.XSLT.Transformer {
|
||||
}
|
||||
return instantiate(otherwise, context)
|
||||
}
|
||||
|
||||
func builtInRule(_ xnode: PureXML.XPath.Node, mode: String?, _ context: XSLTContext) -> [ResultItem] {
|
||||
switch xnode {
|
||||
case let .tree(node):
|
||||
switch node.kind {
|
||||
case .element, .document:
|
||||
applyTemplates(to: node.children.map { .tree($0) }, mode: mode, parameters: [], context)
|
||||
case .text, .cdata:
|
||||
[.node(.text(node.value))]
|
||||
default:
|
||||
[]
|
||||
}
|
||||
case let .attribute(_, attribute):
|
||||
[.node(.text(attribute.value))]
|
||||
case let .namespace(_, _, uri):
|
||||
[.node(.text(uri))]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ extension PureXML.XSLT {
|
||||
let stylesheet: Stylesheet
|
||||
let root: PureXML.Model.TreeNode
|
||||
let documentLoader: (String) -> String?
|
||||
private let keyIndex: PureXML.XSLT.KeyIndex
|
||||
private let keyIndexes = PureXML.XSLT.KeyIndexCache()
|
||||
private let termination = Termination()
|
||||
private let matchCache = MatchCache()
|
||||
private let documentCache = PureXML.XSLT.DocumentCache()
|
||||
@ -28,7 +28,16 @@ extension PureXML.XSLT {
|
||||
self.stylesheet = stylesheet
|
||||
self.root = root
|
||||
self.documentLoader = documentLoader
|
||||
keyIndex = PureXML.XSLT.Library.buildKeyIndex(stylesheet: stylesheet, root: root)
|
||||
}
|
||||
|
||||
/// The key index for `documentRoot`, built on first use (keys apply
|
||||
/// per document: the source and each document() load have their own).
|
||||
private func keyIndex(for documentRoot: PureXML.Model.TreeNode) -> PureXML.XSLT.KeyIndex {
|
||||
let identity = ObjectIdentifier(documentRoot)
|
||||
if let cached = keyIndexes.indexes[identity] { return cached }
|
||||
let built = PureXML.XSLT.Library.buildKeyIndex(stylesheet: stylesheet, root: documentRoot)
|
||||
keyIndexes.indexes[identity] = built
|
||||
return built
|
||||
}
|
||||
|
||||
func run() -> PureXML.Model.Node {
|
||||
@ -81,14 +90,14 @@ extension PureXML.XSLT {
|
||||
private func patternFunctions() -> PureXML.XPath.FunctionTable {
|
||||
PureXML.XSLT.Library.table(
|
||||
current: .tree(root),
|
||||
keys: keyIndex,
|
||||
keys: { keyIndex(for: $0) },
|
||||
loader: documentLoader,
|
||||
decimalFormats: stylesheet.decimalFormats,
|
||||
documents: documentCache,
|
||||
)
|
||||
}
|
||||
|
||||
fileprivate func applyTemplates(
|
||||
func applyTemplates(
|
||||
to nodes: [PureXML.XPath.Node],
|
||||
mode: String?,
|
||||
parameters: [Binding],
|
||||
@ -135,24 +144,6 @@ extension PureXML.XSLT {
|
||||
return instantiate(template.body, context)
|
||||
}
|
||||
|
||||
private func builtInRule(_ xnode: PureXML.XPath.Node, mode: String?, _ context: XSLTContext) -> [ResultItem] {
|
||||
switch xnode {
|
||||
case let .tree(node):
|
||||
switch node.kind {
|
||||
case .element, .document:
|
||||
applyTemplates(to: node.children.map { .tree($0) }, mode: mode, parameters: [], context)
|
||||
case .text, .cdata:
|
||||
[.node(.text(node.value))]
|
||||
default:
|
||||
[]
|
||||
}
|
||||
case let .attribute(_, attribute):
|
||||
[.node(.text(attribute.value))]
|
||||
case let .namespace(_, _, uri):
|
||||
[.node(.text(uri))]
|
||||
}
|
||||
}
|
||||
|
||||
func instantiate(_ body: [Instruction], _ context: XSLTContext) -> [ResultItem] {
|
||||
var items: [ResultItem] = []
|
||||
var context = context
|
||||
@ -186,7 +177,7 @@ extension PureXML.XSLT {
|
||||
variables: context.variables,
|
||||
functions: PureXML.XSLT.Library.table(
|
||||
current: context.focus,
|
||||
keys: keyIndex,
|
||||
keys: { keyIndex(for: $0) },
|
||||
loader: documentLoader,
|
||||
decimalFormats: stylesheet.decimalFormats,
|
||||
documents: documentCache,
|
||||
|
||||
@ -58,9 +58,6 @@ extend/extend01
|
||||
extend/extend02
|
||||
extend/extend04
|
||||
idkey/idkey09
|
||||
idkey/idkey12
|
||||
idkey/idkey18
|
||||
idkey/idkey21
|
||||
idkey/idkey30
|
||||
idkey/idkey31
|
||||
idkey/idkey43
|
||||
@ -70,7 +67,6 @@ idkey/idkey47
|
||||
idkey/idkey48
|
||||
idkey/idkey50
|
||||
idkey/idkey53
|
||||
idkey/idkey59
|
||||
impincl/impincl07
|
||||
impincl/impincl08
|
||||
impincl/impincl11
|
||||
@ -157,7 +153,6 @@ select/select13
|
||||
select/select16
|
||||
select/select27
|
||||
select/select28
|
||||
select/select59
|
||||
select/select67
|
||||
select/select68
|
||||
select/select75
|
||||
|
||||
Loading…
Reference in New Issue
Block a user