* fix(xslt): resolve string-form document() against the stylesheet element's base URI
A string-form document() reference resolves against the base URI of the
stylesheet element holding it (XSLT 1.0 12.1). For a declaration in an
included or imported file that is the included file's URI, not the top
stylesheet's, so document('data.xml') (or document('')) in an included file
must resolve against the included file.
The parser already tracked each top-level declaration's source-file base but
discarded it after collection. That base is now carried onto the compiled
templates and global variables, into the runtime context, and through to the
function table, so the document() string form resolves against it. Templates
and globals both carry it, so the rule holds wherever the call appears.
Closes Apache Xalan conformance cases mdocs12 and mdocs13.
* fix(xslt): carry base URI into for-each and sort contexts; keep document('') self-referential
Follow-up to the string-form document() base URI work. Three coverage gaps:
- xsl:for-each and xsl:sort evaluate their body and key expressions in a
context derived from, not equal to, the enclosing template's, so a
document() call there resolved against the empty base when the template came
from an included file. Both now inherit the enclosing context's base URI.
- A non-empty base URI made document('') in the top stylesheet resolve to that
base and route through the loader, rather than returning the stylesheet's own
parsed document as before. The parsed stylesheet is now registered under its
base URI, so document('') reaches it without the loader; document('') in an
included file (a different base) still loads that file.
172 lines
7.2 KiB
Swift
172 lines
7.2 KiB
Swift
/// A node queued for sorting: its original offset and computed keys.
|
|
private struct SortEntry {
|
|
let offset: Int
|
|
let node: PureXML.XPath.Node
|
|
let keys: [String]
|
|
}
|
|
|
|
extension PureXML.XSLT.Transformer {
|
|
func sorted(_ nodes: [PureXML.XPath.Node], _ sorts: [PureXML.XSLT.Sort], _ context: XSLTContext) -> [PureXML.XPath.Node] {
|
|
guard !sorts.isEmpty else { return nodes }
|
|
// Keys are computed once per node with the node's own evaluation
|
|
// context (its position and size in the selection, the caller's
|
|
// variables and bindings), so position()-based keys work.
|
|
let keyed = nodes.enumerated().map { offset, xnode -> SortEntry in
|
|
guard let owner = Self.ownerNode(xnode) else { return SortEntry(offset: offset, node: xnode, keys: sorts.map { _ in "" }) }
|
|
var keyContext = XSLTContext(
|
|
node: owner,
|
|
current: xnode.treeNode == nil ? xnode : nil,
|
|
position: offset + 1,
|
|
size: nodes.count,
|
|
variables: context.variables,
|
|
)
|
|
keyContext.namespaces = context.namespaces
|
|
keyContext.baseURI = context.baseURI
|
|
return SortEntry(offset: offset, node: xnode, keys: sorts.map { string($0.select, keyContext) })
|
|
}
|
|
// A lang attribute value template evaluates once per sort, in the
|
|
// caller's context; a known language selects its tailored alphabet.
|
|
let tailorings: [[Character: Int]?] = sorts.map { sort in
|
|
sort.lang.flatMap { PureXML.XSLT.Collation.table(for: avt($0, context)) }
|
|
}
|
|
return keyed.sorted { lhs, rhs in
|
|
for (index, sort) in sorts.enumerated() {
|
|
let order: Int
|
|
if !sort.numeric, sort.caseOrder == nil, let ranks = tailorings[index] {
|
|
let tailored = PureXML.XSLT.Collation.compare(lhs.keys[index], rhs.keys[index], ranks)
|
|
order = sort.descending ? -tailored : tailored
|
|
} else {
|
|
order = Self.compareKeys(lhs.keys[index], rhs.keys[index], sort)
|
|
}
|
|
if order != 0 { return order < 0 }
|
|
}
|
|
return lhs.offset < rhs.offset
|
|
}.map(\.node)
|
|
}
|
|
|
|
static func compareKeys(_ left: String, _ right: String, _ sort: PureXML.XSLT.Sort) -> Int {
|
|
var order: Int
|
|
if sort.numeric {
|
|
let leftNumber = PureXML.XPath.Value.parseNumber(left)
|
|
let rightNumber = PureXML.XPath.Value.parseNumber(right)
|
|
// NaN keys sort before every number (the Xalan order).
|
|
switch (leftNumber.isNaN, rightNumber.isNaN) {
|
|
case (true, true): order = 0
|
|
case (true, false): order = -1
|
|
case (false, true): order = 1
|
|
case (false, false): order = leftNumber == rightNumber ? 0 : (leftNumber < rightNumber ? -1 : 1)
|
|
}
|
|
} else if let caseOrder = sort.caseOrder {
|
|
order = caseInsensitiveCompare(left, right, caseOrder)
|
|
} else {
|
|
order = collate(left, right)
|
|
}
|
|
return sort.descending ? -order : order
|
|
}
|
|
|
|
/// Text sort order: spaces are ignorable at the primary level (Java
|
|
/// Collator semantics, which the conformance golds encode); when the
|
|
/// space-stripped strings tie, the string whose first space comes later
|
|
/// (or that has none) sorts first.
|
|
static func collate(_ left: String, _ right: String) -> Int {
|
|
let strippedLeft = left.filter { $0 != " " }
|
|
let strippedRight = right.filter { $0 != " " }
|
|
if strippedLeft != strippedRight {
|
|
return strippedLeft < strippedRight ? -1 : 1
|
|
}
|
|
let leftSpaces = spacePositions(left)
|
|
let rightSpaces = spacePositions(right)
|
|
if leftSpaces == rightSpaces { return 0 }
|
|
for (leftIndex, rightIndex) in zip(leftSpaces, rightSpaces) where leftIndex != rightIndex {
|
|
return leftIndex > rightIndex ? -1 : 1
|
|
}
|
|
return leftSpaces.count < rightSpaces.count ? -1 : 1
|
|
}
|
|
|
|
private static func spacePositions(_ text: String) -> [Int] {
|
|
text.enumerated().compactMap { $1 == " " ? $0 : nil }
|
|
}
|
|
|
|
func forEach(
|
|
_ select: String,
|
|
_ sorts: [PureXML.XSLT.Sort],
|
|
_ body: [PureXML.XSLT.Instruction],
|
|
_ context: XSLTContext,
|
|
) -> [ResultItem] {
|
|
let nodes = sorted(selectXPathNodes(select, context), sorts, context)
|
|
var items: [ResultItem] = []
|
|
for (offset, xnode) in nodes.enumerated() {
|
|
guard let owner = Self.ownerNode(xnode) else { continue }
|
|
var itemContext = XSLTContext(
|
|
node: owner,
|
|
current: xnode.treeNode == nil ? xnode : nil,
|
|
position: offset + 1,
|
|
size: nodes.count,
|
|
variables: context.variables,
|
|
)
|
|
itemContext.baseURI = context.baseURI
|
|
items += instantiate(body, itemContext)
|
|
}
|
|
return items
|
|
}
|
|
|
|
func chooseInstruction(
|
|
_ whens: [PureXML.XSLT.Branch],
|
|
_ otherwise: [PureXML.XSLT.Instruction],
|
|
_ context: XSLTContext,
|
|
) -> [ResultItem] {
|
|
for branch in whens where boolean(branch.test, context) {
|
|
return instantiate(branch.body, context)
|
|
}
|
|
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))]
|
|
}
|
|
}
|
|
|
|
func message(_ terminate: Bool, _ body: [PureXML.XSLT.Instruction], _ context: XSLTContext) -> [ResultItem] {
|
|
if terminate, termination.message == nil {
|
|
termination.message = Self.text(of: instantiate(body, context))
|
|
}
|
|
return []
|
|
}
|
|
|
|
/// Binds the template's parameters (a passed `with-param` wins over the
|
|
/// declared default) and instantiates its body.
|
|
func instantiateTemplate(
|
|
_ template: PureXML.XSLT.Template,
|
|
_ context: XSLTContext,
|
|
passing parameters: [PureXML.XSLT.Binding],
|
|
from caller: XSLTContext,
|
|
) -> [ResultItem] {
|
|
var context = context
|
|
context.importPrecedence = template.importPrecedence
|
|
context.importRangeLow = template.importRangeLow
|
|
context.namespaces = template.namespaces
|
|
context.baseURI = template.baseURI
|
|
for parameter in template.parameters {
|
|
if let passed = parameters.first(where: { $0.name == parameter.name }) {
|
|
context.variables[parameter.name] = variableValue(passed.select, passed.body, caller)
|
|
} else {
|
|
context.variables[parameter.name] = variableValue(parameter.select, parameter.body, context)
|
|
}
|
|
}
|
|
return instantiate(template.body, context)
|
|
}
|
|
}
|