Two OASIS catalog gaps from the audit: - systemSuffix and uriSuffix entries: an identifier ending in the rule's suffix maps to its URI, longest suffix winning, tried after exact and rewrite matches. - prefer=public|system on the catalog: a system match always wins, and the public identifier is consulted as a fallback only when prefer is public or no system identifier was given (the OASIS rule). The default stays prefer=public. xml:base / RFC 3986 relative-URI resolution of entry URIs and the legacy SGML catalog format remain. Part of #86
149 lines
5.7 KiB
Swift
149 lines
5.7 KiB
Swift
/// Accumulates catalog entries as the tree is walked. File-scope and private: an
|
|
/// internal detail of ``PureXML/Catalog/CatalogParser``.
|
|
private struct CatalogBuilder {
|
|
typealias RewriteRule = PureXML.Catalog.RewriteRule
|
|
typealias DelegateRule = PureXML.Catalog.DelegateRule
|
|
typealias SuffixRule = PureXML.Catalog.SuffixRule
|
|
|
|
var systemMap: [String: String] = [:]
|
|
var publicMap: [String: String] = [:]
|
|
var uriMap: [String: String] = [:]
|
|
var rewriteSystem: [RewriteRule] = []
|
|
var rewriteURI: [RewriteRule] = []
|
|
var delegateSystem: [DelegateRule] = []
|
|
var delegatePublic: [DelegateRule] = []
|
|
var delegateURI: [DelegateRule] = []
|
|
var systemSuffix: [SuffixRule] = []
|
|
var uriSuffix: [SuffixRule] = []
|
|
var nextCatalogs: [String] = []
|
|
var preferPublic = true
|
|
|
|
mutating func add(_ node: PureXML.Model.TreeNode) {
|
|
switch node.name?.localName {
|
|
case "system":
|
|
insert(node, key: "systemId", value: "uri", into: &systemMap)
|
|
case "public":
|
|
insert(node, key: "publicId", value: "uri", into: &publicMap)
|
|
case "uri":
|
|
insert(node, key: "name", value: "uri", into: &uriMap)
|
|
case "rewriteSystem":
|
|
appendRewrite(node, start: "systemIdStartString", into: &rewriteSystem)
|
|
case "rewriteURI":
|
|
appendRewrite(node, start: "uriStartString", into: &rewriteURI)
|
|
case "systemSuffix":
|
|
appendSuffix(node, suffix: "systemIdSuffix", into: &systemSuffix)
|
|
case "uriSuffix":
|
|
appendSuffix(node, suffix: "uriSuffix", into: &uriSuffix)
|
|
default:
|
|
addDelegation(node)
|
|
}
|
|
}
|
|
|
|
private func appendSuffix(
|
|
_ node: PureXML.Model.TreeNode,
|
|
suffix: String,
|
|
into rules: inout [SuffixRule],
|
|
) {
|
|
guard let suffixString = attribute(node, suffix), let uri = attribute(node, "uri") else { return }
|
|
rules.append(SuffixRule(suffixString: suffixString, uri: uri))
|
|
}
|
|
|
|
private mutating func addDelegation(_ node: PureXML.Model.TreeNode) {
|
|
switch node.name?.localName {
|
|
case "delegateSystem":
|
|
appendDelegate(node, start: "systemIdStartString", into: &delegateSystem)
|
|
case "delegatePublic":
|
|
appendDelegate(node, start: "publicIdStartString", into: &delegatePublic)
|
|
case "delegateURI":
|
|
appendDelegate(node, start: "uriStartString", into: &delegateURI)
|
|
case "nextCatalog":
|
|
if let catalog = attribute(node, "catalog") { nextCatalogs.append(catalog) }
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func appendDelegate(
|
|
_ node: PureXML.Model.TreeNode,
|
|
start: String,
|
|
into rules: inout [DelegateRule],
|
|
) {
|
|
guard let startString = attribute(node, start), let catalog = attribute(node, "catalog") else { return }
|
|
rules.append(DelegateRule(startString: startString, catalog: catalog))
|
|
}
|
|
|
|
private func insert(
|
|
_ node: PureXML.Model.TreeNode,
|
|
key: String,
|
|
value: String,
|
|
into map: inout [String: String],
|
|
) {
|
|
guard let identifier = attribute(node, key), let uri = attribute(node, value) else { return }
|
|
map[identifier] = uri
|
|
}
|
|
|
|
private func appendRewrite(
|
|
_ node: PureXML.Model.TreeNode,
|
|
start: String,
|
|
into rules: inout [RewriteRule],
|
|
) {
|
|
guard let startString = attribute(node, start), let rewrite = attribute(node, "rewritePrefix") else { return }
|
|
rules.append(RewriteRule(startString: startString, rewritePrefix: rewrite))
|
|
}
|
|
|
|
private func attribute(_ node: PureXML.Model.TreeNode, _ name: String) -> String? {
|
|
node.attributes.first { $0.name.localName == name || $0.name.description == name }?.value
|
|
}
|
|
|
|
func resolver() -> PureXML.Catalog.Resolver {
|
|
PureXML.Catalog.Resolver(
|
|
systemMap: systemMap,
|
|
publicMap: publicMap,
|
|
uriMap: uriMap,
|
|
rewriteSystem: rewriteSystem,
|
|
rewriteURI: rewriteURI,
|
|
delegateSystem: delegateSystem,
|
|
delegatePublic: delegatePublic,
|
|
delegateURI: delegateURI,
|
|
systemSuffix: systemSuffix,
|
|
uriSuffix: uriSuffix,
|
|
nextCatalogs: nextCatalogs,
|
|
preferPublic: preferPublic,
|
|
)
|
|
}
|
|
}
|
|
|
|
extension PureXML.Catalog {
|
|
/// Parses an OASIS XML catalog document into a ``Resolver``. Entries are
|
|
/// matched by local name, so the catalog namespace is not required; `group`
|
|
/// and `catalog` elements are recursed into.
|
|
enum CatalogParser {
|
|
static func parse(_ xml: String) throws -> Resolver {
|
|
let root = try PureXML.parseTree(xml)
|
|
var builder = CatalogBuilder()
|
|
if let catalog = catalogElement(root), prefer(of: catalog) == "system" {
|
|
builder.preferPublic = false
|
|
}
|
|
collect(root, into: &builder)
|
|
return builder.resolver()
|
|
}
|
|
|
|
private static func catalogElement(_ node: PureXML.Model.TreeNode) -> PureXML.Model.TreeNode? {
|
|
node.kind == .element ? node : node.children.first { $0.kind == .element }
|
|
}
|
|
|
|
private static func prefer(of node: PureXML.Model.TreeNode) -> String? {
|
|
node.attributes.first { $0.name.localName == "prefer" }?.value
|
|
}
|
|
|
|
private static func collect(_ node: PureXML.Model.TreeNode, into builder: inout CatalogBuilder) {
|
|
for child in node.children where child.kind == .element {
|
|
builder.add(child)
|
|
if child.name?.localName == "group" || child.name?.localName == "catalog" {
|
|
collect(child, into: &builder)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|