PureXML/Sources/XSLT/XSLTDocumentCache.swift
Mihaela Mihaljevic b6af499558
fix(xslt): resolve document() references against the correct base URI (closes 3 Xalan reluri cases) (#256)
* fix(xslt): resolve document() references against the correct base URI

The document() function resolved every relative URI reference against the
single global base, so a reference relative to a document that document()
itself had loaded from a subdirectory could not be found. XSLT 1.0 12.1
defines the base per reference: the two-argument form against the second
argument's first node, the node-set form against each reference node's own
document.

Each loaded document now records the URI it came from as its base, and a
relative reference resolves against that base before the loader applies its
own source-document base. The common one-argument string form is unchanged
(its base is empty, deferring to the loader as before).

Closes Apache Xalan conformance cases reluri09, reluri10, reluri11.

* fix(xslt): record base URI for document() fragment selections

A fragment selection (document('x#frag')) is returned as a detached subtree
whose root identity differs from the cached whole-document tree, so its base
URI was not recorded. Using such a node as the base for a relative document()
reference then resolved against the empty base instead of the fragment's
document. Record the base on each fragment subtree's own root through a shared
helper, so baseURI(of:) finds it whichever node the reference is taken from.
2026-06-24 01:30:54 +02:00

60 lines
3.0 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] = [:]
}
/// 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
}
}