feat: delayed processing of attributes

This commit is contained in:
relikd
2025-11-27 15:27:10 +01:00
parent a28636a7ca
commit 9847ab76d9

View File

@@ -30,9 +30,71 @@ public struct Xml_Parser {
return rv return rv
} }
// MARK: Iterate Elements
/// Helper struct for delayed attribute processing
public struct AttributesDict {
private let element: XmlStartElement
private let lookupFn: (StringPoolRef) -> String
private let namespaces: [StringPoolRef: String]
private let nsNeedsAppend: StringPoolRef?
private let stringPool: StringPool
fileprivate init(_ elem: XmlStartElement, _ pool: StringPool, lookupFn: @escaping (StringPoolRef) -> String, namespaces: [StringPoolRef : String], nsNeedsAppend: StringPoolRef?) {
self.element = elem
self.stringPool = pool
self.lookupFn = lookupFn
self.namespaces = namespaces
self.nsNeedsAppend = nsNeedsAppend
}
/// Create new list of attributes (keeping original order)
public func asList() throws -> [(String, ResValue)] {
var rv: [(String, ResValue)] = []
// if tag is preceded by a namespace, apply ns-attribute
if let uri = nsNeedsAppend, let prefix = namespaces[uri] {
rv.append(("xmlns:\(prefix)", ResValue(type: .String, data: uri)))
}
// generate attributes
for attr in try element.attributes() {
var key = lookupFn(attr.name)
if let prefix = namespaces[attr.ns] {
key = prefix + ":" + key
}
rv.append((key, attr.typedValue))
}
return rv
}
/// Create new dictionary of attributes (loosing original order)
public func asDict() throws -> [String: ResValue] {
var rv: [String: ResValue] = [:]
// if tag is preceded by a namespace, apply ns-attribute
if let uri = nsNeedsAppend, let prefix = namespaces[uri] {
rv["xmlns:\(prefix)"] = ResValue(type: .String, data: uri)
}
// generate attributes
for attr in try element.attributes() {
var key = lookupFn(attr.name)
if let prefix = namespaces[attr.ns] {
key = prefix + ":" + key
}
rv[key] = attr.typedValue
}
return rv
}
/// Same as `asDict()` but calls `resolve()` on all values.
/// @Note String-values will contain unescaped quotes (`""`).
public func asDictStr() throws -> [String: String] {
// No `lookupFn` because attribute values are likely unique
try asDict().mapValues { $0.resolve(stringPool)}
}
}
/// Iterate over whole tree and call `start` and `end` blocks for each element. /// Iterate over whole tree and call `start` and `end` blocks for each element.
/// Each `start` block contains the `tag` name and its attribute `(name, value)` pairs. /// Each `start` block contains the `tag` name and its attribute `(name, value)` pairs.
public func iterElements(_ start: (_ startTag: String, _ attributes: [(String, ResValue)]) -> Void, _ end: (_ endTag: String) -> Void) throws { public func iterElements(_ start: (_ startTag: String, _ attributes: AttributesDict) throws -> Void, _ end: (_ endTag: String) throws -> Void) throws {
// cache StringPool lookups for faster access // cache StringPool lookups for faster access
var _lookupTable: [StringPoolRef: String] = [:] var _lookupTable: [StringPoolRef: String] = [:]
func lookup(_ index: StringPoolRef) -> String { func lookup(_ index: StringPoolRef) -> String {
@@ -67,34 +129,25 @@ public struct Xml_Parser {
case .XmlStartElement: case .XmlStartElement:
let elem = XmlStartElement(chunk) let elem = XmlStartElement(chunk)
var attrs: [(String, ResValue)] = [] let attrs = AttributesDict(elem, stringPool, lookupFn: lookup(_:), namespaces: namespaces, nsNeedsAppend: nsOnNextTag)
// if tag is preceded by a namespace, apply ns-attribute
if let uri = nsOnNextTag, let prefix = namespaces[uri] {
attrs.append(("xmlns:\(prefix)", ResValue(type: .String, data: uri)))
}
nsOnNextTag = nil nsOnNextTag = nil
// generate attributes try start(lookup(elem.name), attrs)
for attr in try elem.attributes() {
var key = lookup(attr.name)
if let prefix = namespaces[attr.ns] {
key = prefix + ":" + key
}
attrs.append((key, attr.typedValue))
}
start(lookup(elem.name), attrs)
case .XmlEndElement: case .XmlEndElement:
end(lookup(XmlEndElement(chunk).name)) try end(lookup(XmlEndElement(chunk).name))
default: default:
throw AXMLError("Dont know how to handle \(chunk.type) (offset: \(chunk.index(.startOfChunk)))") throw AXMLError("Dont know how to handle \(chunk.type) (offset: \(chunk.index(.startOfChunk)))")
} }
} }
} }
}
// MARK: XML string
/// Return data as XML string. // MARK: - XML string
extension Xml_Parser {
/// Convenience getter to return data as XML string.
public func xmlString(prettyPrint: Bool = true, collapseEmptyElements: Bool = true, indent: String = " ") throws -> String { public func xmlString(prettyPrint: Bool = true, collapseEmptyElements: Bool = true, indent: String = " ") throws -> String {
var rv = #"<?xml version="1.0" encoding="utf-8"?>"# var rv = #"<?xml version="1.0" encoding="utf-8"?>"#
func newLine() -> String { func newLine() -> String {
@@ -109,7 +162,7 @@ public struct Xml_Parser {
rv.append(">") rv.append(">")
} }
rv.append(newLine() + "<" + startTag) rv.append(newLine() + "<" + startTag)
for (key, attr) in attrs { for (key, attr) in try attrs.asList() {
// yes, strings can contain quotes. Thus we must escape them. // yes, strings can contain quotes. Thus we must escape them.
// Hint: no `replace` because that is only available with Foundation // Hint: no `replace` because that is only available with Foundation
let val = attr.dataType == .String let val = attr.dataType == .String
@@ -138,7 +191,7 @@ public struct Xml_Parser {
} }
// MARK: SVG converter // MARK: - SVG converter
extension Xml_Parser { extension Xml_Parser {
/// Same as `xmlString()` but converts tag names and attribute names to SVG standard. /// Same as `xmlString()` but converts tag names and attribute names to SVG standard.
@@ -162,7 +215,7 @@ extension Xml_Parser {
if lastOpen { if lastOpen {
rv.append(">") rv.append(">")
} }
var dict = attrs.reduce(into: [:]) { $0[$1.0] = $1.1 } var dict = try attrs.asDict()
if startTag == "vector" { if startTag == "vector" {
rv.append(newLine() + #"<svg version="1.1" xmlns="http://www.w3.org/2000/svg""#) rv.append(newLine() + #"<svg version="1.1" xmlns="http://www.w3.org/2000/svg""#)
if let val = dict.removeValue(forKey: "android:height") { if let val = dict.removeValue(forKey: "android:height") {