feat(xslt): prolog ordering, html doctype, simplified syntax, CDATA splitting
The XML declaration precedes the document type declaration; a public-only doctype is emitted by the html method and ignored by the xml method, which requires doctype-system; a literal result element carrying xsl:version compiles as a single match="/" template (the 2.3 simplified syntax); and cdata-section text containing ]]> splits across section boundaries. Parser entry and Parts move to XSLTParserEntry.swift for the file caps. Xalan baseline 218 -> 204 (output 34 -> 21).
This commit is contained in:
parent
1646128b35
commit
9945fb74a5
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- 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).
|
||||
- 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).
|
||||
|
||||
@ -1,48 +1,8 @@
|
||||
typealias XSLTTree = PureXML.Model.TreeNode
|
||||
|
||||
/// The accumulating declarations of a stylesheet as its top-level elements are
|
||||
/// compiled, plus the folding of an included or imported sub-stylesheet. File
|
||||
/// scope and private.
|
||||
private struct Parts {
|
||||
var templates: [PureXML.XSLT.Template] = []
|
||||
var globals: [PureXML.XSLT.Instruction] = []
|
||||
var keys: [PureXML.XSLT.Key] = []
|
||||
var output = PureXML.XSLT.Output()
|
||||
var stripSpace: Set<String> = []
|
||||
var preserveSpace: Set<String> = []
|
||||
var attributeSets: [String: PureXML.XSLT.AttributeSet] = [:]
|
||||
var decimalFormats: [String: PureXML.XSLT.DecimalFormat] = [:]
|
||||
var namespaceAliases: [String: PureXML.XSLT.NamespaceAlias] = [:]
|
||||
|
||||
var stylesheet: PureXML.XSLT.Stylesheet {
|
||||
PureXML.XSLT.Stylesheet(
|
||||
templates: templates,
|
||||
globals: globals,
|
||||
keys: keys,
|
||||
output: output,
|
||||
stripSpace: stripSpace,
|
||||
preserveSpace: preserveSpace,
|
||||
attributeSets: attributeSets,
|
||||
decimalFormats: decimalFormats,
|
||||
namespaceAliases: namespaceAliases,
|
||||
)
|
||||
}
|
||||
|
||||
/// Folds a sub-stylesheet in: an import has lower precedence, so its globals
|
||||
/// come before and its output is overridden by this stylesheet's.
|
||||
mutating func fold(_ sub: PureXML.XSLT.Stylesheet?, isImport: Bool) {
|
||||
guard let sub else { return }
|
||||
templates += sub.templates
|
||||
keys += sub.keys
|
||||
stripSpace.formUnion(sub.stripSpace)
|
||||
preserveSpace.formUnion(sub.preserveSpace)
|
||||
attributeSets.merge(sub.attributeSets) { mine, _ in mine }
|
||||
decimalFormats.merge(sub.decimalFormats) { mine, _ in mine }
|
||||
namespaceAliases.merge(sub.namespaceAliases) { mine, _ in mine }
|
||||
globals = isImport ? sub.globals + globals : globals + sub.globals
|
||||
output = isImport ? sub.output.merged(with: output) : output.merged(with: sub.output)
|
||||
}
|
||||
}
|
||||
// The accumulating declarations of a stylesheet as its top-level elements are
|
||||
// compiled, plus the folding of an included or imported sub-stylesheet. File
|
||||
// scope and private.
|
||||
|
||||
/// XSLTTree helpers for the XSLT parser. File-scope and private.
|
||||
enum XSLTNode {
|
||||
@ -91,23 +51,8 @@ extension PureXML.XSLT {
|
||||
/// vocabulary is recognized by namespace or the `xsl` prefix; other elements
|
||||
/// are literal result elements.
|
||||
enum XSLTParser {
|
||||
static func parse(_ xsl: String, loader: (String) -> String? = { _ in nil }) throws -> Stylesheet {
|
||||
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)
|
||||
}
|
||||
|
||||
private static func stylesheetElement(_ root: XSLTTree) -> XSLTTree? {
|
||||
guard let top = XSLTNode.elementChildren(root).first, XSLTNode.isXSL(top),
|
||||
XSLTNode.localName(top) == "stylesheet" || XSLTNode.localName(top) == "transform"
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return top
|
||||
}
|
||||
|
||||
/// Compiles a stylesheet element, folding in `xsl:include`/`xsl:import`.
|
||||
private static func compile(_ top: XSLTTree, loader: (String) -> String?, precedence: Int) -> Stylesheet {
|
||||
static func compile(_ top: XSLTTree, loader: (String) -> String?, precedence: Int) -> Stylesheet {
|
||||
var parts = Parts()
|
||||
for child in XSLTNode.elementChildren(top) where XSLTNode.isXSL(child) {
|
||||
absorb(child, into: &parts, loader: loader, precedence: precedence)
|
||||
@ -227,7 +172,7 @@ extension PureXML.XSLT {
|
||||
node.children.compactMap(instruction)
|
||||
}
|
||||
|
||||
private static func instruction(_ node: XSLTTree) -> Instruction? {
|
||||
static func instruction(_ node: XSLTTree) -> Instruction? {
|
||||
switch node.kind {
|
||||
case .text, .cdata:
|
||||
let value = node.value
|
||||
|
||||
77
Sources/XSLT/XSLTParserEntry.swift
Normal file
77
Sources/XSLT/XSLTParserEntry.swift
Normal file
@ -0,0 +1,77 @@
|
||||
/// The accumulating halves of a stylesheet under compilation.
|
||||
struct Parts {
|
||||
var templates: [PureXML.XSLT.Template] = []
|
||||
var globals: [PureXML.XSLT.Instruction] = []
|
||||
var keys: [PureXML.XSLT.Key] = []
|
||||
var output = PureXML.XSLT.Output()
|
||||
var stripSpace: Set<String> = []
|
||||
var preserveSpace: Set<String> = []
|
||||
var attributeSets: [String: PureXML.XSLT.AttributeSet] = [:]
|
||||
var decimalFormats: [String: PureXML.XSLT.DecimalFormat] = [:]
|
||||
var namespaceAliases: [String: PureXML.XSLT.NamespaceAlias] = [:]
|
||||
|
||||
var stylesheet: PureXML.XSLT.Stylesheet {
|
||||
PureXML.XSLT.Stylesheet(
|
||||
templates: templates,
|
||||
globals: globals,
|
||||
keys: keys,
|
||||
output: output,
|
||||
stripSpace: stripSpace,
|
||||
preserveSpace: preserveSpace,
|
||||
attributeSets: attributeSets,
|
||||
decimalFormats: decimalFormats,
|
||||
namespaceAliases: namespaceAliases,
|
||||
)
|
||||
}
|
||||
|
||||
/// Folds a sub-stylesheet in: an import has lower precedence, so its globals
|
||||
/// come before and its output is overridden by this stylesheet's.
|
||||
mutating func fold(_ sub: PureXML.XSLT.Stylesheet?, isImport: Bool) {
|
||||
guard let sub else { return }
|
||||
templates += sub.templates
|
||||
keys += sub.keys
|
||||
stripSpace.formUnion(sub.stripSpace)
|
||||
preserveSpace.formUnion(sub.preserveSpace)
|
||||
attributeSets.merge(sub.attributeSets) { mine, _ in mine }
|
||||
decimalFormats.merge(sub.decimalFormats) { mine, _ in mine }
|
||||
namespaceAliases.merge(sub.namespaceAliases) { mine, _ in mine }
|
||||
globals = isImport ? sub.globals + globals : globals + sub.globals
|
||||
output = isImport ? sub.output.merged(with: output) : output.merged(with: sub.output)
|
||||
}
|
||||
}
|
||||
|
||||
extension PureXML.XSLT.XSLTParser {
|
||||
static func parse(_ xsl: String, loader: (String) -> String? = { _ in nil }) throws -> PureXML.XSLT.Stylesheet {
|
||||
let root = try PureXML.parseTree(xsl, limits: .init(allowDoctype: true))
|
||||
if let top = stylesheetElement(root) {
|
||||
return compile(top, loader: loader, precedence: 0)
|
||||
}
|
||||
// Simplified syntax (2.3): a literal result element carrying
|
||||
// xsl:version becomes the body of a match="/" template.
|
||||
let literal = XSLTNode.elementChildren(root).first
|
||||
let isSimplified = literal?.attributes.contains { $0.name.prefix == "xsl" && $0.name.localName == "version" } ?? false
|
||||
if isSimplified, let literal, let body = instruction(literal) {
|
||||
var parts = Parts()
|
||||
parts.templates.append(PureXML.XSLT.Template(
|
||||
match: "/",
|
||||
name: nil,
|
||||
mode: nil,
|
||||
priority: 0,
|
||||
importPrecedence: 0,
|
||||
parameters: [],
|
||||
body: [body],
|
||||
))
|
||||
return parts.stylesheet
|
||||
}
|
||||
throw PureXML.XSLT.XSLTError.notAStylesheet
|
||||
}
|
||||
|
||||
static func stylesheetElement(_ root: XSLTTree) -> XSLTTree? {
|
||||
guard let top = XSLTNode.elementChildren(root).first, XSLTNode.isXSL(top),
|
||||
XSLTNode.localName(top) == "stylesheet" || XSLTNode.localName(top) == "transform"
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return top
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,20 @@ public extension PureXML.XSLT {
|
||||
if sheet.output.omitXMLDeclaration == nil { emitOptions.includeXMLDeclaration = true }
|
||||
body = PureXML.serialize(prepared, options: emitOptions)
|
||||
}
|
||||
return doctype(for: result, sheet.output) + RawText.resolve(body)
|
||||
// The XML declaration stays first; the doctype follows it.
|
||||
let resolved = RawText.resolve(body)
|
||||
let documentType = doctype(for: result, sheet.output, method: method)
|
||||
guard !documentType.isEmpty else { return resolved }
|
||||
if resolved.hasPrefix("<?xml "), let end = rangeOf("?>", in: resolved[...]) {
|
||||
var prolog = String(resolved[..<end.upperBound])
|
||||
var rest = String(resolved[end.upperBound...])
|
||||
if rest.hasPrefix("\n") {
|
||||
prolog += "\n"
|
||||
rest.removeFirst()
|
||||
}
|
||||
return prolog + documentType + rest
|
||||
}
|
||||
return documentType + resolved
|
||||
}
|
||||
|
||||
/// The default output method (16.1): html when the first element of the
|
||||
@ -94,9 +107,9 @@ public extension PureXML.XSLT {
|
||||
return .document(children.map { withCDATASections($0, names) })
|
||||
case let .element(element):
|
||||
let wrap = names.contains(element.name.localName) || names.contains(element.name.description)
|
||||
let children = element.children.map { child -> PureXML.Model.Node in
|
||||
if wrap, case let .text(value) = child { return .cdata(value) }
|
||||
return withCDATASections(child, names)
|
||||
let children = element.children.flatMap { child -> [PureXML.Model.Node] in
|
||||
if wrap, case let .text(value) = child { return cdataNodes(value) }
|
||||
return [withCDATASections(child, names)]
|
||||
}
|
||||
return .element(.init(name: element.name, attributes: element.attributes, children: children))
|
||||
default:
|
||||
@ -104,6 +117,41 @@ public extension PureXML.XSLT {
|
||||
}
|
||||
}
|
||||
|
||||
/// Text as CDATA nodes, with any "]]>" split across section boundaries
|
||||
/// ("]]" ends one section, ">" starts the next) so no section contains
|
||||
/// the terminator.
|
||||
private static func cdataNodes(_ text: String) -> [PureXML.Model.Node] {
|
||||
var parts: [String] = []
|
||||
var remainder = text[...]
|
||||
while let range = rangeOf("]]>", in: remainder) {
|
||||
parts.append(String(remainder[..<range.lowerBound]))
|
||||
remainder = remainder[range.upperBound...]
|
||||
}
|
||||
parts.append(String(remainder))
|
||||
guard parts.count > 1 else { return [.cdata(text)] }
|
||||
return parts.enumerated().map { offset, part in
|
||||
let lead = offset > 0 ? ">" : ""
|
||||
let trail = offset < parts.count - 1 ? "]]" : ""
|
||||
return .cdata(lead + part + trail)
|
||||
}
|
||||
}
|
||||
|
||||
/// The first occurrence of `needle` in `haystack` (a Foundation-free
|
||||
/// substring search).
|
||||
private static func rangeOf(_ needle: String, in haystack: Substring) -> Range<String.Index>? {
|
||||
guard !needle.isEmpty else { return nil }
|
||||
var start = haystack.startIndex
|
||||
while let first = haystack[start...].firstIndex(of: needle.first ?? " ") {
|
||||
let end = haystack.index(first, offsetBy: needle.count, limitedBy: haystack.endIndex)
|
||||
if haystack[first...].hasPrefix(needle), let end {
|
||||
return first ..< end
|
||||
}
|
||||
start = haystack.index(after: first)
|
||||
if start >= haystack.endIndex { break }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Wraps `loader` so a relative URI (from `document()`, `xsl:include`, or
|
||||
/// `xsl:import`) is resolved against `baseURI` before loading. An empty base
|
||||
/// leaves URIs as written.
|
||||
@ -114,10 +162,19 @@ public extension PureXML.XSLT {
|
||||
|
||||
/// The `<!DOCTYPE …>` prologue for the result's root element when the output
|
||||
/// declares `doctype-system` (optionally with `doctype-public`), else empty.
|
||||
private static func doctype(for result: PureXML.Model.Node, _ output: Output) -> String {
|
||||
guard let system = output.doctypeSystem, let name = rootName(of: result) else { return "" }
|
||||
let external = output.doctypePublic.map { "PUBLIC \"\($0)\" \"\(system)\"" } ?? "SYSTEM \"\(system)\""
|
||||
return "<!DOCTYPE \(name) \(external)>\n"
|
||||
private static func doctype(for result: PureXML.Model.Node, _ output: Output, method: String) -> String {
|
||||
guard let name = rootName(of: result) else { return "" }
|
||||
if let system = output.doctypeSystem {
|
||||
let external = output.doctypePublic.map { "PUBLIC \"\($0)\" \"\(system)\"" } ?? "SYSTEM \"\(system)\""
|
||||
return "<!DOCTYPE \(name) \(external)>\n"
|
||||
}
|
||||
// Without a system identifier only the html method emits a doctype
|
||||
// (16.1: the xml method requires doctype-system; 16.2 allows
|
||||
// public-only for html).
|
||||
if method == "html", let publicID = output.doctypePublic {
|
||||
return "<!DOCTYPE \(name) PUBLIC \"\(publicID)\">\n"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private static func rootName(of node: PureXML.Model.Node) -> String? {
|
||||
|
||||
@ -53,7 +53,6 @@ copy/copy48
|
||||
copy/copy56
|
||||
copy/copy58
|
||||
copy/copy60
|
||||
embed/embed03
|
||||
expression/expression02
|
||||
extend/extend01
|
||||
extend/extend02
|
||||
@ -126,37 +125,24 @@ node/node12
|
||||
node/node13
|
||||
node/node15
|
||||
numbering/numbering63
|
||||
output/output13
|
||||
output/output15
|
||||
output/output17
|
||||
output/output22
|
||||
output/output26
|
||||
output/output27
|
||||
output/output30
|
||||
output/output32
|
||||
output/output33
|
||||
output/output34
|
||||
output/output35
|
||||
output/output36
|
||||
output/output37
|
||||
output/output38
|
||||
output/output39
|
||||
output/output40
|
||||
output/output41
|
||||
output/output48
|
||||
output/output49
|
||||
output/output59
|
||||
output/output60
|
||||
output/output63
|
||||
output/output64
|
||||
output/output65
|
||||
output/output70
|
||||
output/output72
|
||||
output/output73
|
||||
output/output74
|
||||
output/output75
|
||||
output/output80
|
||||
output/output82
|
||||
output/output86
|
||||
output/output89
|
||||
output/output90
|
||||
|
||||
Loading…
Reference in New Issue
Block a user