PureXML/Sources/XSLT/XSLTModel.swift
Mihaela Mihaljevic 37ea8fcbfa
fix(xslt): resolve string-form document() against the stylesheet element's base URI (closes mdocs12/13) (#257)
* fix(xslt): resolve string-form document() against the stylesheet element's base URI

A string-form document() reference resolves against the base URI of the
stylesheet element holding it (XSLT 1.0 12.1). For a declaration in an
included or imported file that is the included file's URI, not the top
stylesheet's, so document('data.xml') (or document('')) in an included file
must resolve against the included file.

The parser already tracked each top-level declaration's source-file base but
discarded it after collection. That base is now carried onto the compiled
templates and global variables, into the runtime context, and through to the
function table, so the document() string form resolves against it. Templates
and globals both carry it, so the rule holds wherever the call appears.

Closes Apache Xalan conformance cases mdocs12 and mdocs13.

* fix(xslt): carry base URI into for-each and sort contexts; keep document('') self-referential

Follow-up to the string-form document() base URI work. Three coverage gaps:

- xsl:for-each and xsl:sort evaluate their body and key expressions in a
  context derived from, not equal to, the enclosing template's, so a
  document() call there resolved against the empty base when the template came
  from an included file. Both now inherit the enclosing context's base URI.

- A non-empty base URI made document('') in the top stylesheet resolve to that
  base and route through the loader, rather than returning the stylesheet's own
  parsed document as before. The parsed stylesheet is now registered under its
  base URI, so document('') reaches it without the loader; document('') in an
  included file (a different base) still loads that file.
2026-06-24 02:03:32 +02:00

313 lines
14 KiB
Swift

public extension PureXML.XSLT {
/// One part of an attribute value template: literal text, or an embedded XPath
/// expression written `{expr}`.
enum ValuePart: Sendable {
case literal(String)
case expression(String)
}
/// An attribute value template (a string with `{expr}` substitutions).
typealias ValueTemplate = [ValuePart]
/// A literal-result-element attribute, whose value is an attribute value
/// template.
struct LiteralAttribute: Sendable {
public var name: PureXML.Model.QualifiedName
public var value: ValueTemplate
}
/// Which case sorts first among strings equal apart from case.
enum CaseOrder: Sendable {
case upperFirst
case lowerFirst
}
/// A sort specification for `apply-templates`/`for-each`.
struct Sort: Sendable {
public var select: String
public var descending: Bool
public var numeric: Bool
/// `case-order`: when set, the comparison is case-insensitive and this
/// breaks ties; when nil, the comparison is the default codepoint order.
public var caseOrder: CaseOrder?
/// `lang` as an attribute value template; a tailored alphabet order
/// applies when the evaluated language has one.
public var lang: ValueTemplate?
public init(select: String, descending: Bool, numeric: Bool, caseOrder: CaseOrder? = nil, lang: ValueTemplate? = nil) {
self.lang = lang
self.select = select
self.descending = descending
self.numeric = numeric
self.caseOrder = caseOrder
}
}
/// One instruction of a template body (a sequence constructor item).
indirect enum Instruction: Sendable {
/// Literal character data copied to the result.
case literalText(String)
/// `xsl:value-of`: the string value of an XPath expression.
case valueOf(select: String, raw: Bool)
/// `xsl:apply-templates` with an optional node-set selection, mode, sorts,
/// and passed parameters.
case applyTemplates(select: String?, mode: String?, sorts: [Sort], parameters: [Binding])
/// `xsl:for-each` over a node-set.
case forEach(select: String, sorts: [Sort], body: [Instruction])
/// `xsl:if`.
case ifInstruction(test: String, body: [Instruction])
/// `xsl:choose` with its `when` branches and optional `otherwise`.
case choose(whens: [Branch], otherwise: [Instruction])
/// A literal result element, copied to the output with its attributes and
/// any `use-attribute-sets`.
case literalElement(name: PureXML.Model.QualifiedName, attributes: [LiteralAttribute], namespaces: [String: String], useAttributeSets: [String], body: [Instruction])
/// `xsl:element` whose name is an attribute value template, with any
/// `use-attribute-sets`.
case element(name: ValueTemplate, namespace: ValueTemplate?, namespaces: [String: String], useAttributeSets: [String], body: [Instruction])
/// `xsl:attribute` whose name is an attribute value template.
case attribute(name: ValueTemplate, namespace: ValueTemplate?, namespaces: [String: String], body: [Instruction])
/// `xsl:copy`: a shallow copy of the context node.
case copy(useAttributeSets: [String], body: [Instruction])
/// `xsl:copy-of`: a deep copy of a selected node-set.
case copyOf(select: String)
/// `xsl:call-template` by name with passed parameters.
case callTemplate(name: String, parameters: [Binding])
/// `xsl:variable` bound for the rest of the sequence constructor.
case variable(name: String, select: String?, body: [Instruction])
/// `xsl:number`: a generated sequence number for the context node.
case number(NumberSpec)
/// `xsl:comment`: a comment node whose text is the instantiated body.
case comment(body: [Instruction])
/// `xsl:processing-instruction` whose target is `name` and whose data is
/// the instantiated body.
case processingInstruction(name: ValueTemplate, body: [Instruction])
/// `xsl:message`: instantiates its body as a diagnostic. `terminate` ends
/// the transformation with that text; it produces no result-tree output.
case message(terminate: Bool, body: [Instruction])
/// `xsl:apply-imports`: re-applies templates to the current node, in the
/// current mode, considering only templates of lower import precedence.
case applyImports
/// The `xsl:fallback` content of an unrecognized XSLT element, instantiated
/// in its place under forwards-compatible processing.
case fallback(body: [Instruction])
}
/// The `xsl:output` controls that shape serialization: the output method, and
/// the declaration and indentation settings. Each is nil when unspecified, so
/// the serializer falls back to the caller's options.
struct Output: Sendable {
public var method: String?
public var indent: Bool?
public var omitXMLDeclaration: Bool?
public var encoding: String?
public var version: String?
public var standalone: Bool?
/// The `doctype-public` / `doctype-system` identifiers: when either is set,
/// a `<!DOCTYPE>` for the result's root element is emitted before it.
public var doctypePublic: String?
public var doctypeSystem: String?
/// Element names whose text content is emitted in `<![CDATA[…]]>` sections.
public var cdataSectionElements: Set<String>
public init(
method: String? = nil,
indent: Bool? = nil,
omitXMLDeclaration: Bool? = nil,
encoding: String? = nil,
version: String? = nil,
standalone: Bool? = nil,
doctypePublic: String? = nil,
doctypeSystem: String? = nil,
cdataSectionElements: Set<String> = [],
) {
self.method = method
self.indent = indent
self.omitXMLDeclaration = omitXMLDeclaration
self.encoding = encoding
self.version = version
self.standalone = standalone
self.doctypePublic = doctypePublic
self.doctypeSystem = doctypeSystem
self.cdataSectionElements = cdataSectionElements
}
/// This output's settings with `other`'s non-nil settings layered over them.
func merged(with other: Output) -> Output {
Output(
method: other.method ?? method,
indent: other.indent ?? indent,
omitXMLDeclaration: other.omitXMLDeclaration ?? omitXMLDeclaration,
encoding: other.encoding ?? encoding,
version: other.version ?? version,
standalone: other.standalone ?? standalone,
doctypePublic: other.doctypePublic ?? doctypePublic,
doctypeSystem: other.doctypeSystem ?? doctypeSystem,
cdataSectionElements: cdataSectionElements.union(other.cdataSectionElements),
)
}
}
/// An `xsl:namespace-alias` target: the result namespace (and its prefix) that
/// a literal result element's stylesheet namespace is rewritten to on output.
struct NamespaceAlias: Sendable {
public var uri: String?
public var prefix: String?
public init(uri: String?, prefix: String?) {
self.uri = uri
self.prefix = prefix
}
}
/// The attribute bundle of an `xsl:number` instruction: the numbering
/// level, the count/from patterns, an explicit value expression, the
/// format string, and the digit grouping settings.
struct NumberSpec: Sendable {
var level: String
var count: String?
var from: String?
var value: String?
var format: String
var groupingSeparator: String?
var groupingSize: Int?
}
/// The symbols an `xsl:decimal-format` sets for `format-number`: those used in
/// the picture (digit places, separators, percent) and in the output. The
/// defaults are the XSLT standard format.
struct DecimalFormat: Sendable {
public var decimalSeparator: Character = "."
public var groupingSeparator: Character = ","
public var percent: Character = "%"
public var perMille: Character = "\u{2030}"
public var zeroDigit: Character = "0"
public var digit: Character = "#"
public var patternSeparator: Character = ";"
public var minusSign: Character = "-"
public var infinity: String = "Infinity"
public var notANumber: String = "NaN"
public init() {}
}
/// An `xsl:attribute-set`: the `xsl:attribute` instructions it contributes and
/// the other attribute sets it includes (`use-attribute-sets`).
struct AttributeSet: Sendable {
public var attributes: [Instruction]
public var use: [String]
public init(attributes: [Instruction], use: [String]) {
self.attributes = attributes
self.use = use
}
}
/// An `xsl:key` declaration: a name, the nodes it indexes (a match pattern),
/// and the key value of each (a `use` expression).
struct Key: Sendable {
public var name: String
public var match: String
public var use: String
}
/// One `xsl:when` branch of a choose.
struct Branch: Sendable {
public var test: String
public var body: [Instruction]
}
/// A name binding: an `xsl:param` declaration (with a default) or an
/// `xsl:with-param` value, supplied either by `select` or by a body.
struct Binding: Sendable {
public var name: String
public var select: String?
public var body: [Instruction]
}
/// A template rule: a match pattern and/or a name, a mode, a priority, its
/// import precedence (higher wins, used to resolve `xsl:import`), its declared
/// parameters, and a body.
struct Template: Sendable {
public var match: String?
public var name: String?
public var mode: String?
public var priority: Double
public var importPrecedence: Int
/// The lowest precedence inside this template's stylesheet's own
/// import subtree: apply-imports considers only templates with
/// precedence in [importRangeLow, importPrecedence).
public var importRangeLow: Int = 0
public var parameters: [Binding]
public var body: [Instruction]
/// The stylesheet xmlns bindings in scope at the template, for
/// resolving prefixes in its match pattern and expressions.
public var namespaces: [String: String] = [:]
/// The base URI of the stylesheet element this template was parsed from
/// (the including/imported file's URI), against which a relative
/// `document()` reference in its body resolves (XSLT 1.0 12.1).
public var baseURI: String = ""
}
/// A top-level variable or parameter paired with the base URI of the
/// stylesheet element it was parsed from, so a relative `document()`
/// reference in its select expression resolves against the right file
/// (XSLT 1.0 12.1) even after an include or import is folded in.
struct GlobalDeclaration: Sendable {
public var instruction: Instruction
public var baseURI: String
}
/// A compiled stylesheet: its template rules, global variables, keys, output
/// controls, and the `xsl:strip-space`/`xsl:preserve-space` name tests (with
/// `xsl:include`/`xsl:import` already folded in).
struct Stylesheet: Sendable {
public var templates: [Template]
public var globals: [GlobalDeclaration]
public var keys: [Key]
public var output: Output
/// Element name tests whose whitespace-only text children are stripped from
/// the source (an NCName, a qualified name, or `*`).
public var stripSpace: Set<String>
/// Element name tests that keep their whitespace, overriding `stripSpace`.
public var preserveSpace: Set<String>
/// Named `xsl:attribute-set` declarations, keyed by name.
public var attributeSets: [String: [AttributeSet]]
/// `xsl:decimal-format` declarations for `format-number`, keyed by name; the
/// empty key is the default (unnamed) format.
public var decimalFormats: [String: DecimalFormat]
/// The names of top-level xsl:param globals (overridable by
/// caller-supplied parameter values; xsl:variable globals are not).
public var parameterNames: Set<String> = []
/// Whether the stylesheet mentions `disable-output-escaping` anywhere,
/// so the serializer only post-processes raw-text markers when the
/// feature can actually be in play (a source document that happens to
/// contain the private-use marker characters is otherwise untouched).
public var usesRawText: Bool = false
/// `xsl:namespace-alias` rewrites, keyed by the stylesheet namespace URI
/// (the empty key is the no-namespace/default case).
public var namespaceAliases: [String: NamespaceAlias]
public init(
templates: [Template],
globals: [GlobalDeclaration],
keys: [Key],
output: Output,
stripSpace: Set<String> = [],
preserveSpace: Set<String> = [],
attributeSets: [String: [AttributeSet]] = [:],
decimalFormats: [String: DecimalFormat] = [:],
namespaceAliases: [String: NamespaceAlias] = [:],
) {
self.templates = templates
self.globals = globals
self.keys = keys
self.output = output
self.stripSpace = stripSpace
self.preserveSpace = preserveSpace
self.attributeSets = attributeSets
self.decimalFormats = decimalFormats
self.namespaceAliases = namespaceAliases
}
}
}