144 lines
6.3 KiB
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),
|
|
)
|
|
}
|
|
}
|
|
}
|