PureXML/Sources/XSLT/XSLTParserInstructions.swift

144 lines
6.3 KiB
Swift

/// Instruction and template-body compilation, split out so the parser enum
/// stays within the type-body and file length budgets.
extension PureXML.XSLT.XSLTParser {
// MARK: Bodies and instructions
static func body(_ node: XSLTTree) -> [PureXML.XSLT.Instruction] {
// Adjacent text and CDATA children are one text node in the data
// model, so a whitespace-only run is dropped (XSLT 1.0 3.4) only when
// the WHOLE coalesced run is whitespace: " <![CDATA[x]]> " keeps its
// spaces because the run "x" surrounds them with non-whitespace.
// `xml:space="preserve"` in scope keeps a whitespace-only run too.
let preserve = preservesWhitespace(node)
var result: [PureXML.XSLT.Instruction] = []
var run = ""
func flush() {
if !run.isEmpty, preserve || run.unicodeScalars.contains(where: { !PureXML.Parsing.XMLCharacter.isWhitespace($0) }) {
result.append(.literalText(run))
}
run = ""
}
for child in node.children {
if child.kind == .text || child.kind == .cdata {
run += child.value
} else {
flush()
if let instruction = instruction(child) {
result.append(instruction)
}
}
}
flush()
return result
}
static func instruction(_ node: XSLTTree) -> PureXML.XSLT.Instruction? {
switch node.kind {
case .text, .cdata:
let value = node.value
return value.allSatisfy { $0 == " " || $0 == "\t" || $0 == "\n" || $0 == "\r" } ? nil : .literalText(value)
case .element:
return XSLTNode.isXSL(node) ? xslInstruction(node) : literalElement(node)
default:
return nil
}
}
private static func xslInstruction(_ node: XSLTTree) -> PureXML.XSLT.Instruction? {
if let known = simpleInstruction(node) ?? structuralInstruction(node) {
return known
}
// An unrecognized XSLT element instantiates its xsl:fallback children
// (forwards-compatible processing); with none, it is dropped.
let fallback = XSLTNode.children(node, named: "fallback").flatMap(body)
return fallback.isEmpty ? nil : .fallback(body: fallback)
}
private static func simpleInstruction(_ node: XSLTTree) -> PureXML.XSLT.Instruction? {
switch XSLTNode.localName(node) {
case "value-of": .valueOf(
select: XSLTNode.attribute(node, "select") ?? "",
raw: XSLTNode.attribute(node, "disable-output-escaping") == "yes",
)
case "apply-templates": .applyTemplates(
select: XSLTNode.attribute(node, "select"),
mode: expandedMode(node),
sorts: sorts(node),
parameters: withParameters(node),
)
case "copy-of": .copyOf(select: XSLTNode.attribute(node, "select") ?? "")
case "call-template": .callTemplate(
name: expandedDeclaredName(node),
parameters: withParameters(node),
)
case "text": .literalText(
XSLTNode.attribute(node, "disable-output-escaping") == "yes"
? PureXML.XSLT.RawText.marked(node.stringValue)
: node.stringValue,
)
case "variable", "param": variable(node)
case "copy": .copy(useAttributeSets: useAttributeSets(node), body: body(node))
case "message": .message(terminate: XSLTNode.attribute(node, "terminate") == "yes", body: body(node))
case "apply-imports": .applyImports
default: nil
}
}
static func variable(_ node: XSLTTree) -> PureXML.XSLT.Instruction {
.variable(name: expandedDeclaredName(node), select: XSLTNode.attribute(node, "select"), body: body(node))
}
static func choose(_ node: XSLTTree) -> PureXML.XSLT.Instruction {
let whens = XSLTNode.children(node, named: "when").map { branch in
PureXML.XSLT.Branch(test: XSLTNode.attribute(branch, "test") ?? "", body: body(branch))
}
let otherwise = XSLTNode.children(node, named: "otherwise").first.map(body) ?? []
return .choose(whens: whens, otherwise: otherwise)
}
private static func literalElement(_ node: XSLTTree) -> PureXML.XSLT.Instruction {
guard let name = node.name else { return .literalText("") }
// xmlns declarations and the special xsl:* attributes (use-attribute-sets,
// version, exclude-result-prefixes …) are not copied to the output.
let attributes = node.attributes
.filter { $0.name.prefix != "xmlns" && !($0.name.prefix == nil && $0.name.localName == "xmlns") && $0.name.prefix != "xsl" }
.map { PureXML.XSLT.LiteralAttribute(name: $0.name, value: valueTemplate($0.value)) }
return .literalElement(
name: name,
attributes: attributes,
namespaces: copiedNamespaces(node),
useAttributeSets: useAttributeSets(node),
body: body(node),
)
}
/// The whitespace-separated names of `[xsl:]use-attribute-sets` on `node`.
static func useAttributeSets(_ node: XSLTTree) -> [String] {
// Each referenced set is keyed by expanded QName, matching the
// expansion applied to the attribute-set declaration's name.
(XSLTNode.attribute(node, "use-attribute-sets") ?? "")
.split(whereSeparator: \.isWhitespace)
.map { expandedQName(String($0), at: node) }
}
private static func caseOrder(_ node: XSLTTree) -> PureXML.XSLT.CaseOrder? {
switch XSLTNode.attribute(node, "case-order") {
case "upper-first": .upperFirst
case "lower-first": .lowerFirst
default: nil
}
}
static func sorts(_ node: XSLTTree) -> [PureXML.XSLT.Sort] {
XSLTNode.children(node, named: "sort").map { sort in
PureXML.XSLT.Sort(
select: XSLTNode.attribute(sort, "select") ?? ".",
descending: XSLTNode.attribute(sort, "order") == "descending",
numeric: XSLTNode.attribute(sort, "data-type") == "number",
caseOrder: caseOrder(sort),
lang: XSLTNode.attribute(sort, "lang").map(valueTemplate),
)
}
}
}