-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
38 changed files
with
809 additions
and
679 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import Foundation | ||
import pugixml | ||
|
||
public extension Document { | ||
/// A set of namespace names that are referenced in the document but have not been declared. | ||
/// | ||
/// - Note: This property helps identify undeclared namespaces that need to be resolved to generate XML output. | ||
var undeclaredNamespaceNames: Set<String> { | ||
Set(pendingNamespaceRecords.flatMap(\.value.namespaceNames)) | ||
} | ||
} | ||
|
||
internal extension Document { | ||
typealias Prefix = NamespaceDeclaration.Prefix | ||
|
||
static let xmlNamespace = (prefix: Prefix("xml"), name: "http://www.w3.org/XML/1998/namespace") | ||
static let xmlnsNamespace = (prefix: Prefix("xmlns"), name: "http://www.w3.org/2000/xmlns/") | ||
|
||
struct NamespaceDeclaration { | ||
var node: pugi.xml_node | ||
var prefix: Prefix | ||
var namespaceName: String | ||
|
||
enum Prefix: Hashable { | ||
case defaultNamespace | ||
case named (String) | ||
|
||
init(_ string: String?) { | ||
self = if let string { .named(string) } else { .defaultNamespace } | ||
} | ||
|
||
var string: String? { | ||
switch self { | ||
case .named (let string): string | ||
case .defaultNamespace: nil | ||
} | ||
} | ||
} | ||
} | ||
|
||
func namespaceDeclarationCount(for node: Node? = nil) -> Int { | ||
namespaceDeclarationsByPrefix.reduce(0) { result, item in | ||
result + item.value.filter { if let node { $0.node == node.node } else { true } }.count | ||
} | ||
} | ||
|
||
func namespaceName(forPrefix prefix: Prefix, in element: pugi.xml_node) -> String? { | ||
if prefix == Self.xmlNamespace.prefix { return Self.xmlNamespace.name } | ||
if prefix == Self.xmlnsNamespace.prefix { return Self.xmlnsNamespace.name } | ||
|
||
guard let candidates = namespaceDeclarationsByPrefix[prefix] else { | ||
return nil | ||
} | ||
|
||
// Optimization: If the only candidate is the root element, then it's guaranteed to be right | ||
if candidates.count == 1, candidates[0].node == pugiDocument.documentElement { | ||
return candidates[0].namespaceName | ||
} | ||
|
||
var node = element | ||
while(!node.empty()) { | ||
for candidate in candidates where candidate.node == node { | ||
return candidate.namespaceName | ||
} | ||
node = node.parent() | ||
} | ||
return nil | ||
} | ||
|
||
func namespacePrefix(forName name: String, in element: pugi.xml_node) -> Prefix? { | ||
if name == Self.xmlNamespace.name { return Self.xmlNamespace.prefix } | ||
if name == Self.xmlnsNamespace.name { return Self.xmlnsNamespace.prefix } | ||
|
||
guard let candidates = namespaceDeclarationsByName[name], !candidates.isEmpty else { | ||
return nil | ||
} | ||
|
||
// Optimization: If the only candidate is the root element, and the found prefix | ||
// is the only declaration for that prefix (no shadowing), then we've found our match | ||
if candidates.count == 1, candidates[0].node == pugiDocument.documentElement { | ||
let prefix = candidates[0].prefix | ||
if namespaceDeclarationsByPrefix[prefix]?.count == 1 { | ||
return candidates[0].prefix | ||
} | ||
} | ||
|
||
var node = element | ||
while(!node.empty()) { | ||
for candidate in candidates where candidate.node == node { | ||
if namespaceName(forPrefix: candidate.prefix, in: element) == name { | ||
return candidate.prefix | ||
} | ||
} | ||
node = node.parent() | ||
} | ||
return nil | ||
} | ||
|
||
func resetNamespaceDeclarationCache() { | ||
namespaceDeclarationsByName = [:] | ||
namespaceDeclarationsByPrefix = [:] | ||
} | ||
|
||
private func addNamespaceDeclarations(for node: pugi.xml_node) { | ||
for attribute in node.attributes { | ||
let name = attribute.name()! | ||
guard strncmp(name, "xmlns", 5) == 0 else { continue } | ||
|
||
let (prefix, localName) = name.qualifiedNameParts | ||
let declaration = NamespaceDeclaration( | ||
node: node, | ||
prefix: prefix == nil ? .defaultNamespace : .named(localName), | ||
namespaceName: String(cString: attribute.value()) | ||
) | ||
namespaceDeclarationsByName[declaration.namespaceName, default: []].append(declaration) | ||
namespaceDeclarationsByPrefix[declaration.prefix, default: []].append(declaration) | ||
} | ||
} | ||
|
||
private func removeNamespaceDeclarations(for nodes: Set<pugi.xml_node>) { | ||
namespaceDeclarationsByName = namespaceDeclarationsByName.mapValues { | ||
$0.filter { !nodes.contains($0.node) } | ||
} | ||
namespaceDeclarationsByPrefix = namespaceDeclarationsByPrefix.mapValues { | ||
$0.filter { !nodes.contains($0.node) } | ||
} | ||
} | ||
|
||
func removeNamespaceDeclarations(for tree: pugi.xml_node, excludingTarget: Bool = false) { | ||
let descendants = Set(tree.descendants.filter { $0.type() == pugi.node_element && (!excludingTarget || $0 != tree) }) | ||
removeNamespaceDeclarations(for: descendants) | ||
} | ||
|
||
func rebuildNamespaceDeclarationCache(for element: Node) { | ||
removeNamespaceDeclarations(for: [element.node]) | ||
addNamespaceDeclarations(for: element.node) | ||
} | ||
|
||
func rebuildNamespaceDeclarationCache() { | ||
resetNamespaceDeclarationCache() | ||
|
||
for node in pugiDocument.documentElement.descendants { | ||
guard node.type() == pugi.node_element else { continue } | ||
addNamespaceDeclarations(for: node) | ||
} | ||
} | ||
|
||
func declaredNamespacesDidChange(for element: Node) { | ||
rebuildNamespaceDeclarationCache(for: element) | ||
for (element, record) in pendingNameRecords(forDescendantsOf: element) { | ||
if record.attemptResolution(for: element, in: self) { | ||
removePendingNameRecord(for: element) | ||
} | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import Foundation | ||
import pugixml | ||
import Bridge | ||
|
||
internal extension Document { | ||
private func save(encoding: String.Encoding = .utf8, | ||
options: OutputOptions = .default, | ||
indentation: String = .fourSpaces, | ||
output: @escaping (Data) -> () | ||
) throws(OutputError) { | ||
let undeclared = undeclaredNamespaceNames | ||
guard undeclared.isEmpty else { | ||
throw .undeclaredNamespaces(undeclared) | ||
} | ||
|
||
xml_document_save_with_block(pugiDocument, indentation, options.rawValue, encoding.pugiEncoding) { buffer, length in | ||
guard let buffer else { return } | ||
output(Data(bytes: buffer, count: length)) | ||
} | ||
} | ||
} | ||
|
||
public extension Document { | ||
/// Saves the XML document to a specified file URL with the given encoding and options. | ||
/// | ||
/// - Parameters: | ||
/// - fileURL: The location where the XML document should be saved. | ||
/// - encoding: The string encoding to use for the file. Defaults to `.utf8`. | ||
/// - options: The options for XML output formatting. Defaults to `.default`. | ||
/// - indentation: The string to use for indentation in the XML output. Defaults to `.fourSpaces`. | ||
/// - Throws: `OutputError` if the document contains undeclared namespaces or if an error occurs during serialization. | ||
/// Also throws any file-related errors encountered while saving to the provided URL. | ||
func save( | ||
to fileURL: URL, | ||
encoding: String.Encoding = .utf8, | ||
options: OutputOptions = .default, | ||
indentation: String = .fourSpaces | ||
) throws { | ||
FileManager().createFile(atPath: fileURL.path, contents: nil) | ||
let fileHandle = try FileHandle(forWritingTo: fileURL) | ||
fileHandle.truncateFile(atOffset: 0) | ||
defer { fileHandle.closeFile() } | ||
|
||
try save(encoding: encoding, options: options, indentation: indentation) { chunk in | ||
fileHandle.write(chunk) | ||
} | ||
} | ||
|
||
/// Generates the XML data representation of the document with specified options. | ||
/// | ||
/// - Parameters: | ||
/// - encoding: The string encoding to use for the output. Defaults to `.utf8`. | ||
/// - options: The options for XML output formatting. Defaults to `.default`. | ||
/// - indentation: The string to use for indentation in the XML output. Defaults to `.fourSpaces`. | ||
/// - Returns: A `Data` object containing the serialized XML representation of the document. | ||
/// - Throws: `OutputError` if the document contains undeclared namespaces or if an error occurs during serialization. | ||
func xmlData( | ||
encoding: String.Encoding = .utf8, | ||
options: OutputOptions = .default, | ||
indentation: String = .fourSpaces | ||
) throws(OutputError) -> Data { | ||
var data = Data() | ||
try save(encoding: encoding, options: options, indentation: indentation) { chunk in | ||
data.append(chunk) | ||
} | ||
return data | ||
} | ||
|
||
/// Generates the XML string representation of the document with specified options. | ||
/// | ||
/// - Parameters: | ||
/// - options: The options for XML output formatting. Defaults to `.default`. | ||
/// - indentation: The string to use for indentation in the XML output. Defaults to `.fourSpaces`. | ||
/// - Returns: A string containing the serialized XML representation of the document. | ||
func xmlString(options: OutputOptions = .default, indentation: String = .fourSpaces) throws -> String { | ||
String(data: try xmlData(encoding: .utf8, options: options, indentation: indentation), encoding: .utf8) ?? "" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.