diff --git a/CHANGELOG.md b/CHANGELOG.md index 35be9be..b04d145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/XSLT/XSLTFunctions.swift b/Sources/XSLT/XSLTFunctions.swift index 44c0cc4..09b2f5f 100644 --- a/Sources/XSLT/XSLTFunctions.swift +++ b/Sources/XSLT/XSLTFunctions.swift @@ -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 = [ + "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 = [ + "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 { diff --git a/Tests/XSLTFunctionTests.swift b/Tests/XSLTFunctionTests.swift new file mode 100644 index 0000000..b23d54c --- /dev/null +++ b/Tests/XSLTFunctionTests.swift @@ -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 { + """ + + + + + """ + } + + @Test("system-property reports the XSLT version and vendor") + func test_systemProperty() throws { + #expect(try transform(valueOf("system-property('xsl:version')"), "") == "1") + #expect(try transform(valueOf("system-property('xsl:vendor')"), "") == "PureXML") + } + + @Test("element-available is true for a supported instruction, false otherwise") + func test_elementAvailable() throws { + #expect(try transform(valueOf("element-available('xsl:if')"), "") == "true") + #expect(try transform(valueOf("element-available('xsl:nonesuch')"), "") == "false") + } + + @Test("function-available is true for known functions, false otherwise") + func test_functionAvailable() throws { + #expect(try transform(valueOf("function-available('concat')"), "") == "true") + #expect(try transform(valueOf("function-available('generate-id')"), "") == "true") + #expect(try transform(valueOf("function-available('made-up')"), "") == "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 = """ + + + + + """ + let differ = """ + + + + + """ + #expect(try transform(same, "") == "true") + #expect(try transform(differ, "") == "false") + } + + @Test("generate-id produces an XML-name-shaped id") + func test_generateIdShape() throws { + let out = try transform(valueOf("generate-id(.)"), "") + #expect(out.hasPrefix("N")) + #expect(out.count > 1) + } +}