PureXML/Sources/XSLT/XSLTDocumentCache.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

70 lines
3.6 KiB
Swift

extension PureXML.XSLT {
/// Wraps a transform's document loader as an entity resolver, so external
/// DTDs and entities referenced by the stylesheet or source resolve
/// through the same channel as document(). The loader is used from a
/// single thread for the duration of one parse.
static func loaderResolver(_ loader: @escaping (String) -> String?) -> PureXML.Parsing.EntityResolver {
let box = LoaderBox(load: loader)
return PureXML.Parsing.EntityResolver(
resolveEntity: { _, id in box.load(id.resolvedSystemID) },
resolveExternalSubset: { id in box.load(id.resolvedSystemID) },
)
}
/// A single-threaded loader carried into the resolver's Sendable
/// closures; XSLT transforms are confined to one thread.
private struct LoaderBox: @unchecked Sendable {
let load: (String) -> String?
}
/// The key indexes a transform has built, one per document root.
final class KeyIndexCache {
var indexes: [ObjectIdentifier: KeyIndex] = [:]
}
/// The documents a transform has loaded, keyed by URI: per the XSLT
/// `document()` definition the same reference returns the identical
/// document, so node identity (and `generate-id()`) is stable across
/// calls within one transform.
final class DocumentCache {
var trees: [String: PureXML.Model.TreeNode] = [:]
var sources: [String: PureXML.Model.Node] = [:]
/// DTD-declared ID attributes per document root identity: element
/// name to the names of its ID-typed attributes. A document with no
/// entry has no IDs (the XPath id() definition needs the DTD).
var idAttributes: [ObjectIdentifier: [String: Set<String>]] = [:]
/// The id() index per document root, built on first use: ID value to
/// its element (the first in document order wins, per ID semantics).
var idIndexes: [ObjectIdentifier: [String: PureXML.Model.TreeNode]] = [:]
/// The base URI of each loaded document, keyed by its root identity: the
/// URI reference it was loaded from. A relative reference in `document()`
/// resolves against the base URI of the node supplying the base (XSLT 1.0
/// 12.1); a root absent here is the source document, whose base the loader
/// already applies, so it resolves against the empty string.
var baseURIs: [ObjectIdentifier: String] = [:]
/// Registers the top stylesheet's parsed document under its base URI, so a
/// `document('')` from the top stylesheet (whose empty reference resolves
/// to that base) reaches it without the loader; `document('')` in an
/// included file, a different base, still loads that file (XSLT 1.0 12.1).
func registerSelfDocument(_ document: PureXML.Model.Node?, at baseURI: String) {
guard !baseURI.isEmpty, let document else { return }
trees[baseURI] = PureXML.Model.TreeNode(document)
sources[baseURI] = document
}
}
/// The ID-typed attributes a document type declares, element name to
/// attribute names (XPath id() resolves through these).
static func declaredIDAttributes(_ documentType: PureXML.Parsing.DocumentType) -> [String: Set<String>] {
var declared: [String: Set<String>] = [:]
for (element, body) in documentType.attributeLists {
let names = PureXML.Validation.AttributeListParser.parse(body)
.filter { $0.type == .id }
.map(\.name)
if !names.isEmpty { declared[element, default: []].formUnion(names) }
}
return declared
}
}