feat(xslt): add generate-id, system-property, *-available functions
generate-id returns a stable per-node identifier (the context node when no argument is given, the empty string for an empty node-set); system-property reports the XSLT version and vendor; element-available and function-available answer against the implemented instruction and function sets; unparsed-entity-uri is a stub. Part of #82.
This commit is contained in:
parent
f34120a70e
commit
322fd013e9
@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- XSLT functions `generate-id`, `system-property`, `element-available`,
|
||||
`function-available`, and an `unparsed-entity-uri` stub (#82). `generate-id`
|
||||
returns a stable per-node identifier (the context node when no argument is
|
||||
given, the empty string for an empty node-set); `system-property` reports the
|
||||
XSLT version (1.0) and vendor; the `*-available` functions answer against the
|
||||
set of implemented instructions and functions.
|
||||
- XSLT `xsl:namespace-alias` (#81), completing the XSLT top-level-element audit. A
|
||||
literal result element (and its attributes) in a stylesheet namespace bound by
|
||||
`stylesheet-prefix` is rewritten on output to the namespace and prefix bound by
|
||||
|
||||
@ -122,8 +122,52 @@ extension PureXML.XSLT {
|
||||
let parsed = try? PureXML.parseTree(text) else { return .nodeSet([]) }
|
||||
return .nodeSet([.tree(parsed)])
|
||||
}
|
||||
.adding("generate-id") { arguments, context in
|
||||
// No argument uses the context node; an explicit empty node-set
|
||||
// is the empty string, per the XSLT definition.
|
||||
let node = arguments.isEmpty ? context.node.treeNode : arguments.first?.nodes?.first?.treeNode
|
||||
guard let node else { return .string("") }
|
||||
return .string("N\(UInt(bitPattern: ObjectIdentifier(node).hashValue))")
|
||||
}
|
||||
.adding("system-property") { arguments, _ in systemProperty(arguments.first?.string ?? "") }
|
||||
.adding("element-available") { arguments, _ in .boolean(instructionNames.contains(localPart(arguments.first?.string ?? ""))) }
|
||||
.adding("function-available") { arguments, _ in .boolean(functionNames.contains(localPart(arguments.first?.string ?? ""))) }
|
||||
.adding("unparsed-entity-uri") { _, _ in .string("") }
|
||||
}
|
||||
|
||||
private static func localPart(_ name: String) -> String {
|
||||
name.split(separator: ":").last.map(String.init) ?? name
|
||||
}
|
||||
|
||||
/// The XSLT system properties: the version is the number 1.0; the vendor
|
||||
/// strings identify this processor. Any other property is the empty string.
|
||||
private static func systemProperty(_ name: String) -> PureXML.XPath.Value {
|
||||
switch localPart(name) {
|
||||
case "version": .number(1.0)
|
||||
case "vendor": .string("PureXML")
|
||||
case "vendor-url": .string("https://github.com/mihaelamj/PureXML")
|
||||
default: .string("")
|
||||
}
|
||||
}
|
||||
|
||||
/// The XSLT instruction elements this processor implements, for
|
||||
/// `element-available`.
|
||||
static let instructionNames: Set<String> = [
|
||||
"value-of", "apply-templates", "apply-imports", "call-template", "for-each", "if", "choose",
|
||||
"when", "otherwise", "element", "attribute", "copy", "copy-of", "text", "number", "comment",
|
||||
"processing-instruction", "variable", "param", "with-param", "sort", "message", "fallback",
|
||||
]
|
||||
|
||||
/// The XPath and XSLT functions this processor implements, for
|
||||
/// `function-available`.
|
||||
static let functionNames: Set<String> = [
|
||||
"last", "position", "count", "id", "local-name", "namespace-uri", "name", "string", "concat",
|
||||
"starts-with", "contains", "substring-before", "substring-after", "substring", "string-length",
|
||||
"normalize-space", "translate", "boolean", "not", "true", "false", "lang", "number", "sum",
|
||||
"floor", "ceiling", "round", "current", "document", "key", "format-number", "generate-id",
|
||||
"system-property", "element-available", "function-available", "unparsed-entity-uri",
|
||||
]
|
||||
|
||||
/// Builds the `xsl:key` index: each declared key maps every node matching
|
||||
/// its pattern to its `use` value.
|
||||
static func buildKeyIndex(stylesheet: Stylesheet, root: PureXML.Model.TreeNode) -> KeyIndex {
|
||||
|
||||
65
Tests/XSLTFunctionTests.swift
Normal file
65
Tests/XSLTFunctionTests.swift
Normal file
@ -0,0 +1,65 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("XSLT functions: generate-id, system-property, *-available")
|
||||
struct XSLTFunctionTests {
|
||||
private let xsl = "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\""
|
||||
|
||||
private func transform(_ stylesheet: String, _ source: String) throws -> String {
|
||||
try PureXML.XSLT.transform(stylesheet: stylesheet, source: source)
|
||||
}
|
||||
|
||||
private func valueOf(_ select: String) -> String {
|
||||
"""
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:output method="text"/>
|
||||
<xsl:template match="/"><xsl:value-of select="\(select)"/></xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
}
|
||||
|
||||
@Test("system-property reports the XSLT version and vendor")
|
||||
func test_systemProperty() throws {
|
||||
#expect(try transform(valueOf("system-property('xsl:version')"), "<x/>") == "1")
|
||||
#expect(try transform(valueOf("system-property('xsl:vendor')"), "<x/>") == "PureXML")
|
||||
}
|
||||
|
||||
@Test("element-available is true for a supported instruction, false otherwise")
|
||||
func test_elementAvailable() throws {
|
||||
#expect(try transform(valueOf("element-available('xsl:if')"), "<x/>") == "true")
|
||||
#expect(try transform(valueOf("element-available('xsl:nonesuch')"), "<x/>") == "false")
|
||||
}
|
||||
|
||||
@Test("function-available is true for known functions, false otherwise")
|
||||
func test_functionAvailable() throws {
|
||||
#expect(try transform(valueOf("function-available('concat')"), "<x/>") == "true")
|
||||
#expect(try transform(valueOf("function-available('generate-id')"), "<x/>") == "true")
|
||||
#expect(try transform(valueOf("function-available('made-up')"), "<x/>") == "false")
|
||||
}
|
||||
|
||||
@Test("generate-id is stable per node and differs between nodes")
|
||||
func test_generateId() throws {
|
||||
// Same node twice is equal; two different nodes are not.
|
||||
let same = """
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:output method="text"/>
|
||||
<xsl:template match="/"><xsl:value-of select="generate-id(r/a) = generate-id(r/a)"/></xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
let differ = """
|
||||
<xsl:stylesheet version="1.0" \(xsl)>
|
||||
<xsl:output method="text"/>
|
||||
<xsl:template match="/"><xsl:value-of select="generate-id(r/a) = generate-id(r/b)"/></xsl:template>
|
||||
</xsl:stylesheet>
|
||||
"""
|
||||
#expect(try transform(same, "<r><a/><b/></r>") == "true")
|
||||
#expect(try transform(differ, "<r><a/><b/></r>") == "false")
|
||||
}
|
||||
|
||||
@Test("generate-id produces an XML-name-shaped id")
|
||||
func test_generateIdShape() throws {
|
||||
let out = try transform(valueOf("generate-id(.)"), "<x/>")
|
||||
#expect(out.hasPrefix("N"))
|
||||
#expect(out.count > 1)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user