├── Sources └── SwiftXML │ ├── Exports.swift │ ├── Iteration │ ├── ConvenienceExtensions.swift │ ├── IteratorProtocols.swift │ ├── SequenceConcatenation.swift │ └── WrappingIterators.swift │ ├── XML │ ├── Namespaces.swift │ ├── Tools.swift │ ├── Transformation.swift │ └── Production.swift │ ├── Parsing │ └── Parsing.swift │ ├── Utilities.swift │ └── Builder │ └── XParseBuilder.swift ├── .gitignore ├── CONTRIBUTING.md ├── Package.swift ├── Tests └── SwiftXMLTests │ ├── ProcessingInstructionIterationTests.swift │ ├── SequenceTypesTest.swift │ ├── UtilitiesTest.swift │ ├── AttributeNamespacesTests.swift │ ├── ToolsTests.swift │ └── FromReadme.swift └── LICENSE /Sources/SwiftXML/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import SwiftXMLInterfaces 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | /.build 4 | /Packages 5 | /Package.resolved 6 | /*.xcodeproj 7 | /.idea 8 | /.swiftpm 9 | /.vscode 10 | /Sources/SwiftXML/main.swift 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By submitting a pull request or committing changes directly, you represent that you have the right to license your contribution to Stefan Springer and the community, and agree that your contributions are licensed under the [SwiftXML 2 | license](LICENSE). 3 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftXML", 8 | platforms: [ 9 | .macOS(.v15), 10 | .iOS(.v17), 11 | .tvOS(.v17), 12 | .watchOS(.v10), 13 | ], 14 | products: [ 15 | // Products define the executables and libraries a package produces, and make them visible to other packages. 16 | .library( 17 | name: "SwiftXML", 18 | targets: ["SwiftXML"]), 19 | ], 20 | dependencies: [ 21 | // Dependencies declare other packages that this package depends on. 22 | .package(url: "https://github.com/stefanspringer1/SwiftXMLParser", from: "7.0.6"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 26 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 27 | .target( 28 | name: "SwiftXML", 29 | dependencies: [ 30 | "SwiftXMLParser", 31 | ] 32 | ), 33 | .testTarget( 34 | name: "SwiftXMLTests", 35 | dependencies: ["SwiftXML"]), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /Sources/SwiftXML/Iteration/ConvenienceExtensions.swift: -------------------------------------------------------------------------------- 1 | //===--- ConvenienceExtensions.swift -------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) and the SwiftXML project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | //===----------------------------------------------------------------------===// 9 | 10 | public protocol Applying {} 11 | 12 | public extension Applying { 13 | 14 | /// Apply an operation on the instance and return the changed instance. 15 | func applying(_ operation: (inout Self) throws -> Void) rethrows -> Self { 16 | var copy = self 17 | try operation(©) 18 | return copy 19 | } 20 | 21 | } 22 | 23 | public protocol Pulling {} 24 | 25 | public extension Pulling { 26 | 27 | /// Apply an operation on the instance and return the changed instance. 28 | func pulling(_ operation: (inout Self) throws -> T) rethrows -> T { 29 | var copy = self 30 | return try operation(©) 31 | } 32 | 33 | } 34 | 35 | public protocol Fullfilling {} 36 | 37 | public extension Fullfilling { 38 | 39 | /// Test if a certain condition is true for the instance, return the instance if the condition is `true`, else return `nil`. 40 | func fullfilling(_ condition: (Self) throws -> Bool) rethrows -> Self? { 41 | return try condition(self) ? self : nil 42 | } 43 | 44 | } 45 | 46 | public protocol Fullfill {} 47 | 48 | public extension Fullfill { 49 | 50 | /// Test if a certain condition is true for the instance, return the result of this test. 51 | func fullfills(_ condition: (Self) throws -> Bool) rethrows -> Bool { 52 | return try condition(self) 53 | } 54 | } 55 | 56 | extension XContent: Applying, Pulling, Fullfilling, Fullfill {} 57 | -------------------------------------------------------------------------------- /Sources/SwiftXML/Iteration/IteratorProtocols.swift: -------------------------------------------------------------------------------- 1 | //===--- IteratorProtocols.swift ------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | #if canImport(FoundationEssentials) 12 | import FoundationEssentials 13 | #else 14 | import Foundation 15 | #endif 16 | 17 | /** 18 | The XNodeIteratorProtocol implements one more features over the IteratorProtocol, 19 | it can go backwards via the function "previous". 20 | */ 21 | public protocol XContentIteratorProtocol { 22 | mutating func next() -> XContent? 23 | mutating func previous() -> XContent? 24 | } 25 | 26 | /** 27 | The XTextIteratorProtocol implements one more features over the IteratorProtocol, 28 | it can go backwards via the function "previous". 29 | */ 30 | public protocol XTextIteratorProtocol { 31 | mutating func next() -> XText? 32 | mutating func previous() -> XText? 33 | } 34 | 35 | /** 36 | The XElementIteratorProtocol implements one more features over the IteratorProtocol, 37 | it can go backwards via the function "previous". 38 | */ 39 | public protocol XElementIteratorProtocol { 40 | mutating func next() -> XElement? 41 | mutating func previous() -> XElement? 42 | } 43 | 44 | /** 45 | XAttributeIteratorProtocol is the version of XNodeIteratorProtocol for 46 | attributes. 47 | */ 48 | protocol XAttributeIteratorProtocol { 49 | mutating func next() -> AttributeProperties? 50 | mutating func previous() -> AttributeProperties? 51 | } 52 | 53 | /** 54 | XProcessingInstructionrotocol is the version of XNodeIteratorProtocol for 55 | processing instructions. 56 | */ 57 | public protocol XProcessingInstructionIteratorProtocol { 58 | mutating func next() -> XProcessingInstruction? 59 | mutating func previous() -> XProcessingInstruction? 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SwiftXML/XML/Namespaces.swift: -------------------------------------------------------------------------------- 1 | //===--- Namespaces.swift -------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | #if canImport(FoundationEssentials) 12 | import FoundationEssentials 13 | #else 14 | import Foundation 15 | #endif 16 | 17 | func getPrefixTranslations(fromPrefixesForNamespaceURIs prefixesForNamespaceURIs: [String:String]?, forNode node: XNode) -> [String:String]? { 18 | if let prefixesForNamespaceURIs, let document = node.document { 19 | var prefixTranslations = [String:String]() 20 | for (namespace,newPrefix) in prefixesForNamespaceURIs { 21 | print(namespace) 22 | print(newPrefix) 23 | if let prefix = document._namespaceURIToPrefix[namespace] { 24 | prefixTranslations[prefix] = newPrefix 25 | } 26 | } 27 | return prefixTranslations 28 | } else { 29 | return nil 30 | } 31 | } 32 | 33 | func getCompletePrefixTranslations( 34 | prefixTranslations: [String:String]? = nil, 35 | prefixesForNamespaceURIs: [String:String]? = nil, 36 | forNode node: XNode 37 | ) -> [String:String]? { 38 | var completePrefixTranslations: [String:String]? 39 | if let prefixTranslationsFromPrefixesForNamespaceURIs = getPrefixTranslations(fromPrefixesForNamespaceURIs: prefixesForNamespaceURIs, forNode: node) { 40 | if let prefixTranslations { 41 | completePrefixTranslations = prefixTranslationsFromPrefixesForNamespaceURIs.merging(prefixTranslations) { (current, _) in current } 42 | } else { 43 | completePrefixTranslations = prefixTranslationsFromPrefixesForNamespaceURIs 44 | } 45 | } else { 46 | completePrefixTranslations = prefixTranslations 47 | } 48 | return completePrefixTranslations 49 | } 50 | -------------------------------------------------------------------------------- /Tests/SwiftXMLTests/ProcessingInstructionIterationTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | import SwiftXML 4 | import SwiftXMLInterfaces 5 | 6 | final class ProcessingInstructionIterationTests: XCTestCase { 7 | 8 | func test1() throws { 9 | 10 | let source = """ 11 | 12 | Blabla. 13 | Blabla. 14 | Blabla. 15 | 16 | """ 17 | 18 | print(">>>>>>>>>>>>>>>>>>>>>>") 19 | let document = try parseXML(fromText: source) 20 | 21 | document.descendants("b").first?.add { 22 | XProcessingInstruction(target: "MyTarget", data: "PI added later with same target.") 23 | } 24 | print("<<<<<<<<<<<<<<<<<<<<<<") 25 | 26 | XCTAssertEqual( 27 | document.processingInstructions(ofTarget: "MyTarget") 28 | .map { $0.data ?? "" }.joined(separator: "\n"), 29 | """ 30 | Hello world! 31 | This has the same target. 32 | PI added later with same target. 33 | """ 34 | ) 35 | 36 | XCTAssertEqual( 37 | document.processingInstructions(ofTarget: "MyTarget", "OtherTarget") 38 | .map { $0.data ?? "" }.joined(separator: "\n"), 39 | """ 40 | Hello world! 41 | This has the same target. 42 | PI added later with same target. 43 | This has another target. 44 | """ 45 | ) 46 | 47 | let firstProcessingInstructionOfTarget = document.processingInstructions(ofTarget: "MyTarget").first?.removed() 48 | XCTAssertTrue(firstProcessingInstructionOfTarget?.document == nil) 49 | 50 | XCTAssertEqual( 51 | document.processingInstructions(ofTarget: "MyTarget") 52 | .map { $0.data ?? "" }.joined(separator: "\n"), 53 | // The first processing instruction of the target is now missing: 54 | """ 55 | This has the same target. 56 | PI added later with same target. 57 | """ 58 | ) 59 | 60 | let anotherDocument = XDocument { 61 | document.descendants("b").last 62 | } 63 | XCTAssertEqual( 64 | document.processingInstructions(ofTarget: "MyTarget") 65 | .map { $0.data ?? "" }.joined(separator: "\n"), 66 | // Now the 2nd processing instruction of the target is also gone from the first document: 67 | """ 68 | PI added later with same target. 69 | """ 70 | ) 71 | 72 | XCTAssertEqual( 73 | anotherDocument.processingInstructions(ofTarget: "MyTarget") 74 | .map { $0.data ?? "" }.joined(separator: "\n"), 75 | // ...it is now in the second document: 76 | """ 77 | This has the same target. 78 | """ 79 | ) 80 | 81 | anotherDocument.add { 82 | firstProcessingInstructionOfTarget 83 | } 84 | 85 | XCTAssertEqual( 86 | anotherDocument.processingInstructions(ofTarget: "MyTarget") 87 | .map { $0.data ?? "" }.joined(separator: "\n"), 88 | // Now we also have the other processing instruction of the target in the other document: 89 | """ 90 | This has the same target. 91 | Hello world! 92 | """ 93 | ) 94 | 95 | anotherDocument.processingInstructions(ofTarget: "MyTarget").remove() 96 | 97 | XCTAssertEqual( 98 | anotherDocument.processingInstructions(ofTarget: "MyTarget") 99 | .map { $0.data ?? "" }.joined(separator: "\n"), 100 | // All processing instructions of the target are now removed in the other document: 101 | """ 102 | """ 103 | ) 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /Tests/SwiftXMLTests/SequenceTypesTest.swift: -------------------------------------------------------------------------------- 1 | //===--- FromReadme.swift ----------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | import XCTest 12 | import class Foundation.Bundle 13 | @testable import SwiftXML 14 | 15 | 16 | final class SequenceTypesTestTests: XCTestCase { 17 | 18 | func testWithoutNames() throws { 19 | 20 | let document = try parseXML(fromText: """ 21 | texttext 22 | """) 23 | 24 | let d = document.children.children("d").first 25 | XCTAssertNotNil(d) 26 | 27 | // previous: 28 | XCTAssertEqual(d?.previousElements.map{ $0.name }.joined(separator: ", "), "c, b, a") 29 | XCTAssertEqual(d?.previousElementsIncludingSelf.map{ $0.name }.joined(separator: ", "), "d, c, b, a") 30 | XCTAssertEqual(d?.previousCloseElements.map{ $0.name }.joined(separator: ", "), "c, b") 31 | XCTAssertEqual(d?.previousCloseElementsIncludingSelf.map{ $0.name }.joined(separator: ", "), "d, c, b") 32 | 33 | // next: 34 | XCTAssertEqual(d?.nextElements.map{ $0.name }.joined(separator: ", "), "e, f, g") 35 | XCTAssertEqual(d?.nextElementsIncludingSelf.map{ $0.name }.joined(separator: ", "), "d, e, f, g") 36 | XCTAssertEqual(d?.nextCloseElements.map{ $0.name }.joined(separator: ", "), "e, f") 37 | XCTAssertEqual(d?.nextCloseElementsIncludingSelf.map{ $0.name }.joined(separator: ", "), "d, e, f") 38 | 39 | } 40 | 41 | func testWithNames() throws { 42 | 43 | let document = try parseXML(fromText: """ 44 | texttexttexttext 45 | """) 46 | 47 | let d = document.children.children("d").first 48 | XCTAssertNotNil(d) 49 | 50 | // previous: 51 | XCTAssertEqual(d?.previousElements("c").map{ $0.name }.joined(separator: ", "), "c, c, c") 52 | XCTAssertEqual(d?.previousElements(while: { $0.name == "c" }).map{ $0.name }.joined(separator: ", "), "c, c, c") 53 | XCTAssertEqual(d?.previousElementsIncludingSelf(while: { $0.name == "c" }).map{ $0.name }.joined(separator: ", "), "") 54 | XCTAssertEqual(d?.previousElementsIncludingSelf(while: { $0.name == "d" || $0.name == "c" }).map{ $0.name }.joined(separator: ", "), "d, c, c, c") 55 | XCTAssertEqual(d?.previousCloseElements(while: { $0.name == "c" }).map{ $0.name }.joined(separator: ", "), "c, c") 56 | XCTAssertEqual(d?.previousCloseElementsIncludingSelf("c").map{ $0.name }.joined(separator: ", "), "c, c") 57 | XCTAssertEqual(d?.previousCloseElementsIncludingSelf(while: { $0.name == "c" }).map{ $0.name }.joined(separator: ", "), "") 58 | XCTAssertEqual(d?.previousCloseElementsIncludingSelf(while: { $0.name == "d" || $0.name == "c" }).map{ $0.name }.joined(separator: ", "), "d, c, c") 59 | 60 | // next: 61 | XCTAssertEqual(d?.nextElements("e").map{ $0.name }.joined(separator: ", "), "e, e, e") 62 | XCTAssertEqual(d?.nextElements(while: { $0.name == "e" }).map{ $0.name }.joined(separator: ", "), "e, e, e") 63 | XCTAssertEqual(d?.nextElementsIncludingSelf(while: { $0.name == "e" }).map{ $0.name }.joined(separator: ", "), "") 64 | XCTAssertEqual(d?.nextElementsIncludingSelf(while: { $0.name == "d" || $0.name == "e" }).map{ $0.name }.joined(separator: ", "), "d, e, e, e") 65 | XCTAssertEqual(d?.nextCloseElements(while: { $0.name == "e" }).map{ $0.name }.joined(separator: ", "), "e, e") 66 | XCTAssertEqual(d?.nextCloseElementsIncludingSelf("e").map{ $0.name }.joined(separator: ", "), "e, e") 67 | XCTAssertEqual(d?.nextCloseElementsIncludingSelf(while: { $0.name == "e" }).map{ $0.name }.joined(separator: ", "), "") 68 | XCTAssertEqual(d?.nextCloseElementsIncludingSelf(while: { $0.name == "d" || $0.name == "e" }).map{ $0.name }.joined(separator: ", "), "d, e, e") 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/SwiftXML/Iteration/SequenceConcatenation.swift: -------------------------------------------------------------------------------- 1 | //===--- SequenceConcatenation.swift --------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | #if canImport(FoundationEssentials) 12 | import FoundationEssentials 13 | #else 14 | import Foundation 15 | #endif 16 | 17 | // elements of names: 18 | 19 | public final class XElementsOfNamesSequence: XElementSequence { 20 | 21 | private let prefix: String? 22 | private let names: [String] 23 | private let document: XDocument 24 | 25 | init(forPrefix prefix: String?, forNames names: [String], forDocument document: XDocument) { 26 | self.prefix = prefix 27 | self.names = names 28 | self.document = document 29 | } 30 | 31 | public override func makeIterator() -> XElementIterator { 32 | return XElementsOfNamesIterator(forPrefix: prefix, forNames: names, forDocument: document) 33 | } 34 | 35 | } 36 | 37 | public final class XElementsOfNamesIterator: XElementIterator { 38 | 39 | private let iterators: [XXBidirectionalElementNameIterator] 40 | private var foundElement = false 41 | private var iteratorIndex = 0 42 | 43 | init(forPrefix prefix: String?, forNames names: [String], forDocument document: XDocument) { 44 | iterators = names.map{ XXBidirectionalElementNameIterator( 45 | elementIterator: XElementsOfSameNameIterator( 46 | document: document, 47 | prefix: prefix, 48 | name: $0, 49 | keepLast: true 50 | ) 51 | ) } 52 | } 53 | 54 | public override func next() -> XElement? { 55 | guard iterators.count > 0 else { return nil } 56 | while true { 57 | if iteratorIndex == iterators.count { 58 | if foundElement { 59 | iteratorIndex = 0 60 | foundElement = false 61 | } 62 | else { 63 | return nil 64 | } 65 | } 66 | let iterator = iterators[iteratorIndex] 67 | if let next = iterator.next() { 68 | foundElement = true 69 | return next 70 | } 71 | else { 72 | iteratorIndex += 1 73 | } 74 | } 75 | } 76 | 77 | } 78 | 79 | // attributes of names: 80 | 81 | public final class XAttributesOfNamesSequence: XAttributeSequence { 82 | 83 | private let attributePrefix: String? 84 | private let names: [String] 85 | private let document: XDocument 86 | 87 | init(withAttributePrefix attributePrefix: String?, forNames names: [String], forDocument document: XDocument) { 88 | self.attributePrefix = attributePrefix 89 | self.names = names 90 | self.document = document 91 | } 92 | 93 | public override func makeIterator() -> XAttributeIterator { 94 | return XAttributesOfNamesIterator(forPrefix: attributePrefix, forNames: names, forDocument: document) 95 | } 96 | 97 | } 98 | 99 | public final class XAttributesOfNamesIterator: XAttributeIterator { 100 | 101 | private let iterators: [XBidirectionalAttributeIterator] 102 | private var foundElement = false 103 | private var iteratorIndex = 0 104 | 105 | init(forPrefix attributePrefix: String?, forNames names: [String], forDocument document: XDocument) { 106 | iterators = names.map { 107 | XBidirectionalAttributeIterator( 108 | forAttributeName: $0, attributeIterator: XAttributesOfSameNameIterator( 109 | document: document, 110 | attributePrefix: attributePrefix, 111 | attributeName: $0, 112 | keepLast: true 113 | ) 114 | ) 115 | } 116 | } 117 | 118 | public override func next() -> XAttributeSpot? { 119 | guard iterators.count > 0 else { return nil } 120 | while true { 121 | if iteratorIndex == iterators.count { 122 | if foundElement { 123 | iteratorIndex = 0 124 | foundElement = false 125 | } 126 | else { 127 | return nil 128 | } 129 | } 130 | let iterator = iterators[iteratorIndex] 131 | if let next = iterator.next() { 132 | foundElement = true 133 | return XAttributeSpot(name: next.name, value: next.value, element: next.element) 134 | } 135 | else { 136 | iteratorIndex += 1 137 | } 138 | } 139 | } 140 | 141 | } 142 | 143 | public final class XProcessingInstructionOfTargetsIterator: XProcessingInstructionIterator { 144 | 145 | private let iterators: [XBidirectionalProcessingInstructionIterator] 146 | private var foundProcessingInstruction = false 147 | private var iteratorIndex = 0 148 | 149 | init(forTargets targets: [String], forDocument document: XDocument) { 150 | iterators = targets.map { 151 | XBidirectionalProcessingInstructionIterator(processingInstructionIterator: XProcessingInstructionOfSameTargetIterator( 152 | document: document, 153 | target: $0, 154 | keepLast: true 155 | ) 156 | ) 157 | } 158 | } 159 | 160 | public override func next() -> XProcessingInstruction? { 161 | guard iterators.count > 0 else { return nil } 162 | while true { 163 | if iteratorIndex == iterators.count { 164 | if foundProcessingInstruction { 165 | iteratorIndex = 0 166 | foundProcessingInstruction = false 167 | } 168 | else { 169 | return nil 170 | } 171 | } 172 | let iterator = iterators[iteratorIndex] 173 | if let next = iterator.next() { 174 | foundProcessingInstruction = true 175 | return next 176 | } 177 | else { 178 | iteratorIndex += 1 179 | } 180 | } 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /Sources/SwiftXML/XML/Tools.swift: -------------------------------------------------------------------------------- 1 | //===--- Tools.swift ------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | #if canImport(FoundationEssentials) 12 | import FoundationEssentials 13 | #else 14 | import Foundation 15 | #endif 16 | 17 | import SwiftXMLInterfaces 18 | import SwiftXMLParser 19 | 20 | /// Info that a correction in the call to `copyXStructure` has to use. 21 | public struct StructureCopyInfo { 22 | public let structure: XContent 23 | public let start: XContent 24 | public let cloneForStart: XContent 25 | public let end: XContent 26 | public let cloneForEnd: XContent 27 | } 28 | 29 | /// Copies the structure from `start` to `end`, optionally up to the `upTo` value. 30 | /// `start` and `end` must have a common ancestor. 31 | /// Returns `nil` if there is no common ancestor. 32 | /// The returned element is a clone of the `upTo` value if a) it is not `nil` 33 | /// and b) `upTo` is an ancestor of the common ancestor or the ancestor itself. 34 | /// Else it is the clone of the common ancestor (but generally with a different 35 | /// content in both cases). The `correction` can do some corrections. 36 | public func copyXStructure(from start: XContent, to end: XContent, upTo: XElement? = nil, correction: ((StructureCopyInfo) -> XContent)? = nil) -> XContent? { 37 | 38 | func addUpTo(fromCopy copy: XContent) -> XContent { 39 | guard let upTo else { return copy } 40 | var result = copy 41 | while let backlink = result.backlink, backlink !== upTo, let parent = backlink.parent { 42 | let parentClone = parent.shallowClone 43 | parentClone.add { result } 44 | result = parentClone 45 | } 46 | return result 47 | } 48 | 49 | if start === end { 50 | let startClone = start.clone 51 | let result = addUpTo(fromCopy: startClone) 52 | if let correction { 53 | return correction(StructureCopyInfo(structure: result, start: start, cloneForStart: startClone, end: start, cloneForEnd: startClone)) 54 | } else { 55 | return result 56 | } 57 | } 58 | 59 | let allAncestorsForStart = Array(start.ancestorsIncludingSelf(untilAndIncluding: { $0 === upTo })) 60 | 61 | guard let commonAncestor = end.ancestorsIncludingSelf(untilAndIncluding: { $0 === upTo }).filter({ ancestor in allAncestorsForStart.contains(where: { $0 === ancestor }) }).first else { 62 | return nil 63 | } 64 | 65 | var ancestorsForStart = start.ancestorsIncludingSelf(until: { $0 === commonAncestor }).reversed() 66 | if ancestorsForStart.last === start { 67 | ancestorsForStart.removeLast() 68 | } 69 | 70 | var ancestorsForEnd = end.ancestorsIncludingSelf(until: { $0 === commonAncestor }).reversed() 71 | if ancestorsForEnd.last === end { 72 | ancestorsForEnd.removeLast() 73 | } 74 | 75 | let startClone = start.clone 76 | 77 | func processAncestorsForStart() -> XContent { 78 | var content: XContent = startClone 79 | var orginalContent = start 80 | while let ancestor = ancestorsForStart.popLast() { 81 | let cloneOfAncestor = ancestor.shallowClone 82 | cloneOfAncestor.add { 83 | content 84 | orginalContent.next.map { $0.clone } 85 | } 86 | content = cloneOfAncestor 87 | orginalContent = ancestor 88 | } 89 | return content 90 | } 91 | 92 | let endClone = end.clone 93 | 94 | func processAncestorsForEnd() -> XContent { 95 | var content: XContent = endClone 96 | var orginalContent = end 97 | while let ancestor = ancestorsForEnd.popLast() { 98 | let cloneOfAncestor = ancestor.shallowClone 99 | cloneOfAncestor.add { 100 | ancestor.content(until: { $0 === orginalContent }).map { $0.clone } 101 | content 102 | } 103 | content = cloneOfAncestor 104 | orginalContent = ancestor 105 | } 106 | return content 107 | } 108 | 109 | let combined = commonAncestor.shallowClone 110 | let structureForStart = processAncestorsForStart() 111 | let structureForEnd = processAncestorsForEnd() 112 | combined.add { 113 | structureForStart 114 | } 115 | let stopForMiddle = structureForEnd.backlink! 116 | for middle in structureForStart.backlink!.next(until: { $0 === stopForMiddle }) { 117 | combined.add { middle.clone } 118 | } 119 | combined.add { 120 | structureForEnd 121 | } 122 | 123 | let result = addUpTo(fromCopy: combined) 124 | 125 | if let correction { 126 | return correction(StructureCopyInfo(structure: result, start: start, cloneForStart: startClone, end: end, cloneForEnd: endClone)) 127 | } else { 128 | return result 129 | } 130 | } 131 | 132 | public struct XDocumentProperties { 133 | 134 | // ----------------------------- 135 | // from the XML declaration: 136 | // ----------------------------- 137 | 138 | /// The XML version text. 139 | public let xmlVersion: String? 140 | 141 | /// The XML version text. 142 | public let encoding: String? 143 | public let standalone: String? 144 | 145 | // ----------------------------- 146 | // from the doctype declaration: 147 | // ----------------------------- 148 | 149 | /// The document name. 150 | public let name: String? 151 | public var publicID: String? 152 | public var systemID: String? 153 | 154 | // ----------------------------- 155 | // the root element: 156 | // ----------------------------- 157 | 158 | /// The root element. 159 | public let root: XElement? 160 | 161 | } 162 | 163 | public extension XDocumentSource { 164 | 165 | /// Get the document properties from the document source without parsing it any further. 166 | /// The root property will be an empty representation of the root element. 167 | /// Note that no namespace is being resolved. 168 | func readDocumentProperties() throws -> XDocumentProperties { 169 | 170 | class PublicIDAndRootReader: XDefaultEventHandler { 171 | 172 | var xmlVersion: String? = nil 173 | var encoding: String? = nil 174 | var standalone: String? = nil 175 | var name: String? = nil 176 | var publicID: String? = nil 177 | var systemID: String? = nil 178 | var root: XElement? = nil 179 | 180 | override func xmlDeclaration(version: String, encoding: String?, standalone: String?, textRange: XTextRange?, dataRange: XDataRange?) -> Bool { 181 | self.xmlVersion = version 182 | self.encoding = encoding 183 | self.standalone = standalone 184 | return true 185 | } 186 | 187 | override func documentTypeDeclarationStart(name: String, publicID: String?, systemID: String?, textRange: XTextRange?, dataRange: XDataRange?) -> Bool { 188 | self.name = name 189 | self.publicID = publicID 190 | self.systemID = systemID 191 | return true 192 | } 193 | 194 | override func elementStart(name: String, attributes: inout [String : String], textRange: XTextRange?, dataRange: XDataRange?) -> Bool { 195 | root = XElement(name, attributes) 196 | return false 197 | } 198 | 199 | } 200 | 201 | let eventHandler = PublicIDAndRootReader() 202 | 203 | try XParser().parse(fromData: self.getData(), eventHandlers: [eventHandler]) 204 | 205 | return XDocumentProperties( 206 | xmlVersion: eventHandler.xmlVersion, 207 | encoding: eventHandler.encoding, 208 | standalone: eventHandler.standalone, 209 | name: eventHandler.name, 210 | publicID: eventHandler.publicID, 211 | systemID: eventHandler.systemID, 212 | root: eventHandler.root 213 | ) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Sources/SwiftXML/XML/Transformation.swift: -------------------------------------------------------------------------------- 1 | //===--- Transformation.swift ---------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | #if canImport(FoundationEssentials) 12 | import FoundationEssentials 13 | #else 14 | import Foundation 15 | #endif 16 | 17 | public typealias XElementAction = (XElement)->() 18 | 19 | public typealias XAttributeAction = (XAttributeSpot)->() 20 | 21 | public struct XRule { 22 | 23 | public let prefix: String? 24 | public let names: [String] 25 | public let action: Any 26 | 27 | #if DEBUG 28 | 29 | public let actionFile: String 30 | public let actionLine: Int 31 | 32 | public init(forPrefix prefix: String? = nil, forElements names: [String], file: String = #file, line: Int = #line, action: @escaping XElementAction) { 33 | self.prefix = prefix 34 | self.names = names 35 | self.action = action 36 | self.actionFile = file 37 | self.actionLine = line 38 | } 39 | 40 | public init(forPrefix prefix: String? = nil, forElements names: String..., file: String = #file, line: Int = #line, action: @escaping XElementAction) { 41 | self.prefix = prefix 42 | self.names = names 43 | self.action = action 44 | self.actionFile = file 45 | self.actionLine = line 46 | } 47 | 48 | public init(forPrefix prefix: String? = nil, forRegisteredAttributes names: [String], file: String = #file, line: Int = #line, action: @escaping XAttributeAction) { 49 | self.prefix = prefix 50 | self.names = names 51 | self.action = action 52 | self.actionFile = file 53 | self.actionLine = line 54 | } 55 | 56 | public init(forPrefix prefix: String? = nil, forRegisteredAttributes names: String..., file: String = #file, line: Int = #line, action: @escaping XAttributeAction) { 57 | self.prefix = prefix 58 | self.names = names 59 | self.action = action 60 | self.actionFile = file 61 | self.actionLine = line 62 | } 63 | 64 | #else 65 | 66 | public init(forPrefix prefix: String? = nil, forElements names: [String], action: @escaping XElementAction) { 67 | self.prefix = prefix 68 | self.names = names 69 | self.action = action 70 | } 71 | 72 | public init(forPrefix prefix: String? = nil, forElements names: String..., action: @escaping XElementAction) { 73 | self.prefix = prefix 74 | self.names = names 75 | self.action = action 76 | } 77 | 78 | public init(forPrefix prefix: String? = nil, forRegisteredAttributes names: [String], action: @escaping XAttributeAction) { 79 | self.prefix = prefix 80 | self.names = names 81 | self.action = action 82 | } 83 | 84 | public init(forPrefix prefix: String? = nil, forRegisteredAttributes names: String..., action: @escaping XAttributeAction) { 85 | self.prefix = prefix 86 | self.names = names 87 | self.action = action 88 | } 89 | 90 | #endif 91 | } 92 | 93 | public protocol XRulesConvertible { 94 | func asXRules() -> [XRule] 95 | } 96 | 97 | extension XRule: XRulesConvertible { 98 | public func asXRules() -> [XRule] { 99 | return [self] 100 | } 101 | } 102 | 103 | extension Optional: XRulesConvertible where Wrapped == XRule { 104 | public func asXRules() -> [XRule] { 105 | switch self { 106 | case .some(let wrapped): return [wrapped] 107 | case .none: return [] 108 | } 109 | } 110 | } 111 | 112 | extension Array: XRulesConvertible where Element == XRule { 113 | public func asXRules() -> [XRule] { 114 | return self 115 | } 116 | } 117 | 118 | 119 | @resultBuilder 120 | public struct XRulesBuilder { 121 | public static func buildBlock(_ components: XRulesConvertible...) -> [XRule] { 122 | return components.flatMap({ $0.asXRules() }) 123 | } 124 | 125 | public static func buildEither(first component: XRulesConvertible) -> [XRule] { 126 | return component.asXRules() 127 | } 128 | 129 | public static func buildEither(second component: XRulesConvertible) -> [XRule] { 130 | return component.asXRules() 131 | } 132 | } 133 | 134 | public class XTransformation { 135 | 136 | let rules: [XRule] 137 | 138 | public init(@XRulesBuilder builder: () -> [XRule]) { 139 | self.rules = builder() 140 | } 141 | 142 | var stopped = false 143 | 144 | public func stop() { 145 | stopped = true 146 | } 147 | 148 | public func execute(inDocument document: XDocument) { 149 | 150 | #if DEBUG 151 | struct AppliedAction { let iterator: any IteratorProtocol; let action: Any; let actionFile: String; let actionLine: Int } 152 | #else 153 | struct AppliedAction { let iterator: any IteratorProtocol; let action: Any } 154 | #endif 155 | 156 | var iteratorsWithAppliedActions = [AppliedAction]() 157 | 158 | for rule in rules { 159 | if let elementAction = rule.action as? XElementAction { 160 | for name in rule.names { 161 | #if DEBUG 162 | iteratorsWithAppliedActions.append(AppliedAction( 163 | iterator: XXBidirectionalElementNameIterator(elementIterator: XElementsOfSameNameIterator(document: document, prefix: rule.prefix, name: name, keepLast: true), keepLast: true), 164 | action: elementAction, 165 | actionFile: rule.actionFile, 166 | actionLine: rule.actionLine 167 | )) 168 | #else 169 | iteratorsWithAppliedActions.append(AppliedAction( 170 | iterator: XXBidirectionalElementNameIterator(elementIterator: XElementsOfSameNameIterator(document: document, prefix: rule.prefix, name: name, keepLast: true), keepLast: true), 171 | action: elementAction 172 | )) 173 | #endif 174 | } 175 | } else if let attributeAction = rule.action as? XAttributeAction { 176 | rule.names.forEach { name in 177 | #if DEBUG 178 | iteratorsWithAppliedActions.append(AppliedAction( 179 | iterator: XBidirectionalAttributeIterator(forAttributeName: name, attributeIterator: XAttributesOfSameNameIterator(document: document, attributePrefix: rule.prefix, attributeName: name, keepLast: true), keepLast: true), 180 | action: attributeAction, 181 | actionFile: rule.actionFile, 182 | actionLine: rule.actionLine 183 | )) 184 | #else 185 | iteratorsWithAppliedActions.append(AppliedAction( 186 | iterator: XBidirectionalAttributeIterator(forAttributeName: name, attributeIterator: XAttributesOfSameNameIterator(document: document, attributePrefix: rule.prefix, attributeName: name, keepLast: true), keepLast: true), 187 | action: attributeAction 188 | )) 189 | #endif 190 | } 191 | } 192 | } 193 | 194 | var working = true; stopped = false 195 | while !stopped && working { 196 | working = false 197 | actions: for appliedAction in iteratorsWithAppliedActions { 198 | if stopped { break actions } 199 | if let iterator = appliedAction.iterator as? XXBidirectionalElementNameIterator, let action = appliedAction.action as? XElementAction { 200 | action: while let next = iterator.next() { 201 | if stopped { break action } 202 | working = true 203 | action(next) 204 | } 205 | } else if let iterator = appliedAction.iterator as? XBidirectionalAttributeIterator, let action = appliedAction.action as? XAttributeAction { 206 | action: while let attribute = iterator.next() { 207 | if stopped { break action } 208 | working = true 209 | action(attribute) 210 | } 211 | } 212 | } 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Tests/SwiftXMLTests/UtilitiesTest.swift: -------------------------------------------------------------------------------- 1 | //===--- UtilitiesTests.swift ---------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | import XCTest 12 | import class Foundation.Bundle 13 | @testable import SwiftXML 14 | 15 | final class UtilitiesTest: XCTestCase { 16 | 17 | func testTwoTieredDictionaryWithStringKeys() throws { 18 | let dictionary = TwoTieredDictionaryWithStringKeys() 19 | dictionary.put(key1: "3", key2: "z", value: "3z") 20 | dictionary.put(key1: "7", key2: "b", value: "7b") 21 | dictionary.put(key1: "2", key2: "u", value: "2u") 22 | dictionary.put(key1: "2", key2: "a", value: "2a") 23 | dictionary.put(key1: "3", key2: "d", value: "3d") 24 | dictionary.put(key1: "7", key2: "c", value: "7c") 25 | dictionary.put(key1: "3", key2: "a", value: "3a") 26 | 27 | XCTAssertEqual(dictionary["7", "b"], "7b") 28 | XCTAssertEqual(dictionary["2", "u"], "2u") 29 | XCTAssertEqual(dictionary["2", "a"], "2a") 30 | XCTAssertEqual(dictionary["3", "d"], "3d") 31 | XCTAssertEqual(dictionary["7", "c"], "7c") 32 | XCTAssertEqual(dictionary["3", "a"], "3a") 33 | 34 | let keys = dictionary.keys.sorted(by: { $0.0 < $1.0 || ($0.0 == $1.0 && $0.1 < $1.1) }) 35 | print(keys) 36 | XCTAssertTrue( 37 | areEqual( 38 | keys, 39 | [ 40 | ("2", "a"), 41 | ("2", "u"), 42 | ("3", "a"), 43 | ("3", "d"), 44 | ("3", "z"), 45 | ("7", "b"), 46 | ("7", "c"), 47 | ] 48 | ) 49 | ) 50 | 51 | let allSorted = dictionary.all.sorted(by: { $0.0 < $1.0 || ($0.0 == $1.0 && $0.1 < $1.1) || ($0.0 == $1.0 && $0.1 == $1.1 && $0.2 < $1.2) }) 52 | print(allSorted) 53 | XCTAssertTrue( 54 | areEqual( 55 | allSorted, 56 | [ 57 | ("2", "a", "2a"), 58 | ("2", "u", "2u"), 59 | ("3", "a", "3a"), 60 | ("3", "d", "3d"), 61 | ("3", "z", "3z"), 62 | ("7", "b", "7b"), 63 | ("7", "c", "7c"), 64 | ] 65 | ) 66 | ) 67 | let sorted = dictionary.sorted 68 | print(sorted) 69 | XCTAssertTrue( 70 | areEqual( 71 | sorted, 72 | [ 73 | ("2", "a", "2a"), 74 | ("2", "u", "2u"), 75 | ("3", "a", "3a"), 76 | ("3", "d", "3d"), 77 | ("3", "z", "3z"), 78 | ("7", "b", "7b"), 79 | ("7", "c", "7c"), 80 | ] 81 | ) 82 | ) 83 | } 84 | 85 | func testThreeTieredDictionaryWithStringKeys() throws { 86 | let dictionary = ThreeTieredDictionaryWithStringKeys() 87 | dictionary.put(key1: "3", key2: "z", key3: "β", value: "3zβ") 88 | dictionary.put(key1: "7", key2: "b", key3: "α", value: "7bα") 89 | dictionary.put(key1: "2", key2: "u", key3: "γ", value: "2uγ") 90 | dictionary.put(key1: "2", key2: "a", key3: "β", value: "2aβ") 91 | dictionary.put(key1: "3", key2: "d", key3: "α", value: "3dα") 92 | dictionary.put(key1: "3", key2: "z", key3: "γ", value: "3zγ") 93 | dictionary.put(key1: "3", key2: "z", key3: "α", value: "3zα") 94 | dictionary.put(key1: "7", key2: "c", key3: "γ", value: "7cγ") 95 | dictionary.put(key1: "3", key2: "a", key3: "β", value: "3aβ") 96 | dictionary.put(key1: "7", key2: "c", key3: "α", value: "7cα") 97 | 98 | XCTAssertEqual(dictionary["3", "z", "β"], "3zβ") 99 | XCTAssertEqual(dictionary["7", "b", "α"], "7bα") 100 | XCTAssertEqual(dictionary["2", "u", "γ"], "2uγ") 101 | XCTAssertEqual(dictionary["2", "a", "β"], "2aβ") 102 | XCTAssertEqual(dictionary["3", "d", "α"], "3dα") 103 | XCTAssertEqual(dictionary["3", "z", "γ"], "3zγ") 104 | XCTAssertEqual(dictionary["3", "z", "α"], "3zα") 105 | XCTAssertEqual(dictionary["7", "c", "γ"], "7cγ") 106 | XCTAssertEqual(dictionary["3", "a", "β"], "3aβ") 107 | XCTAssertEqual(dictionary["7", "c", "α"], "7cα") 108 | 109 | let keys = dictionary.keys.sorted(by: { $0.0 < $1.0 || ($0.0 == $1.0 && $0.1 < $1.1) || ($0.0 == $1.0 && $0.1 == $1.1 && $0.2 < $1.2) }) 110 | print(keys) 111 | XCTAssertTrue( 112 | areEqual( 113 | keys, 114 | [ 115 | ("2", "a", "β"), 116 | ("2", "u", "γ"), 117 | ("3", "a", "β"), 118 | ("3", "d", "α"), 119 | ("3", "z", "α"), 120 | ("3", "z", "β"), 121 | ("3", "z", "γ"), 122 | ("7", "b", "α"), 123 | ("7", "c", "α"), 124 | ("7", "c", "γ"), 125 | ] 126 | ) 127 | ) 128 | 129 | let allSorted = dictionary.all.sorted(by: { $0.0 < $1.0 || ($0.0 == $1.0 && $0.1 < $1.1) || ($0.0 == $1.0 && $0.1 == $1.1 && $0.2 < $1.2) || ($0.0 == $1.0 && $0.1 == $1.1 && $0.2 == $1.2 && $0.3 < $1.3) }) 130 | print(allSorted) 131 | XCTAssertTrue( 132 | areEqual( 133 | allSorted, 134 | [ 135 | ("2", "a", "β", "2aβ"), 136 | ("2", "u", "γ", "2uγ"), 137 | ("3", "a", "β", "3aβ"), 138 | ("3", "d", "α", "3dα"), 139 | ("3", "z", "α", "3zα"), 140 | ("3", "z", "β", "3zβ"), 141 | ("3", "z", "γ", "3zγ"), 142 | ("7", "b", "α", "7bα"), 143 | ("7", "c", "α", "7cα"), 144 | ("7", "c", "γ", "7cγ"), 145 | ] 146 | ) 147 | ) 148 | 149 | let sorted = dictionary.sorted 150 | print(sorted) 151 | XCTAssertTrue( 152 | areEqual( 153 | sorted, 154 | [ 155 | ("2", "a", "β", "2aβ"), 156 | ("2", "u", "γ", "2uγ"), 157 | ("3", "a", "β", "3aβ"), 158 | ("3", "d", "α", "3dα"), 159 | ("3", "z", "α", "3zα"), 160 | ("3", "z", "β", "3zβ"), 161 | ("3", "z", "γ", "3zγ"), 162 | ("7", "b", "α", "7bα"), 163 | ("7", "c", "α", "7cα"), 164 | ("7", "c", "γ", "7cγ"), 165 | ] 166 | ) 167 | ) 168 | } 169 | 170 | } 171 | 172 | fileprivate func areEqual(_ array1: [(String,String)], _ array2: [(String,String)]) -> Bool { 173 | let size = array1.count 174 | guard size == array2.count else { return false } 175 | for i in 0.. Bool { 185 | let size = array1.count 186 | guard size == array2.count else { return false } 187 | for i in 0.. Bool { 198 | let size = array1.count 199 | guard size == array2.count else { return false } 200 | for i in 0.. XContent? { 39 | if prefetched { 40 | prefetched = false 41 | return current 42 | } 43 | current?.removeContentIterator(self) 44 | current = contentIterator.next() 45 | current?.addContentIterator(self) 46 | return current 47 | } 48 | 49 | public override func previous() -> XContent? { 50 | prefetched = false 51 | current?.removeContentIterator(self) 52 | current = contentIterator.previous() 53 | current?.addContentIterator(self) 54 | return current 55 | } 56 | 57 | public func prefetch() { 58 | current?.removeContentIterator(self) 59 | current = contentIterator.next() 60 | current?.addContentIterator(self) 61 | prefetched = true 62 | } 63 | } 64 | 65 | public final class XBidirectionalTextIterator: XTextIterator { 66 | 67 | var previousIterator: XBidirectionalTextIterator? = nil 68 | var nextIterator: XBidirectionalTextIterator? = nil 69 | 70 | public typealias Element = XText 71 | 72 | var textIterator: XTextIteratorProtocol 73 | 74 | public init(textIterator: XTextIteratorProtocol) { 75 | self.textIterator = textIterator } 76 | 77 | weak var current: XText? = nil 78 | var prefetched = false 79 | 80 | public override func next() -> XText? { 81 | if prefetched { 82 | prefetched = false 83 | return current 84 | } 85 | current?.removeTextIterator(self) 86 | current = textIterator.next() 87 | current?.addTextIterator(self) 88 | return current 89 | } 90 | 91 | public override func previous() -> XText? { 92 | prefetched = false 93 | current?.removeTextIterator(self) 94 | current = textIterator.previous() 95 | current?.addTextIterator(self) 96 | return current 97 | } 98 | 99 | public func prefetch() { 100 | current?.removeTextIterator(self) 101 | current = textIterator.next() 102 | current?.addTextIterator(self) 103 | prefetched = true 104 | } 105 | } 106 | 107 | 108 | public final class XBidirectionalElementIterator: XElementIterator { 109 | 110 | var previousIterator: XBidirectionalElementIterator? = nil 111 | var nextIterator: XBidirectionalElementIterator? = nil 112 | 113 | public typealias Element = XElement 114 | 115 | var elementIterator: XElementIteratorProtocol 116 | 117 | public init(elementIterator: XElementIteratorProtocol) { 118 | self.elementIterator = elementIterator 119 | } 120 | 121 | weak var current: XElement? = nil 122 | var prefetched = false 123 | 124 | public override func next() -> XElement? { 125 | if prefetched { 126 | prefetched = false 127 | return current 128 | } 129 | current?.removeElementIterator(self) 130 | current = elementIterator.next() 131 | current?.addElementIterator(self) 132 | return current 133 | } 134 | 135 | public override func previous() -> XElement? { 136 | prefetched = false 137 | current?.removeElementIterator(self) 138 | current = elementIterator.previous() 139 | current?.addElementIterator(self) 140 | return current 141 | } 142 | 143 | public func prefetch() { 144 | current?.removeElementIterator(self) 145 | current = elementIterator.next() 146 | current?.addElementIterator(self) 147 | prefetched = true 148 | } 149 | } 150 | 151 | public final class XXBidirectionalElementNameIterator: XElementIterator { 152 | 153 | public typealias Element = XElement 154 | 155 | var elementIterator: XElementIteratorProtocol 156 | 157 | var keepLast: Bool 158 | 159 | public init(elementIterator: XElementIteratorProtocol, keepLast: Bool = false) { 160 | self.elementIterator = elementIterator 161 | self.keepLast = keepLast 162 | } 163 | 164 | weak var current: XElement? = nil 165 | var prefetched = false 166 | 167 | public override func next() -> XElement? { 168 | if prefetched { 169 | prefetched = false 170 | return current 171 | } 172 | let next = elementIterator.next() 173 | if keepLast && next == nil { 174 | return nil 175 | } 176 | else { 177 | current?.removeNameIterator(self) 178 | current = next 179 | current?.addNameIterator(self) 180 | return current 181 | } 182 | } 183 | 184 | public override func previous() -> XElement? { 185 | prefetched = false 186 | current?.removeNameIterator(self) 187 | current = elementIterator.previous() 188 | current?.addNameIterator(self) 189 | return current 190 | } 191 | 192 | public func prefetch() { 193 | current?.removeNameIterator(self) 194 | current = elementIterator.next() 195 | current?.addNameIterator(self) 196 | prefetched = true 197 | } 198 | } 199 | 200 | public struct XAttributeSpot { public let name: String; public let value: String; public let element: XElement } 201 | 202 | public final class XBidirectionalAttributeIterator: XAttributeIterator { 203 | 204 | var previousIterator: XBidirectionalAttributeIterator? = nil 205 | var nextIterator: XBidirectionalAttributeIterator? = nil 206 | 207 | public typealias Element = XAttributeSpot 208 | 209 | var attributeIterator: XAttributeIteratorProtocol 210 | 211 | var keepLast: Bool 212 | var name: String 213 | 214 | init(forAttributeName name: String, attributeIterator: XAttributeIteratorProtocol, keepLast: Bool = false) { 215 | self.name = name 216 | self.attributeIterator = attributeIterator 217 | self.keepLast = keepLast 218 | } 219 | 220 | weak var current: AttributeProperties? = nil 221 | var prefetched = false 222 | 223 | public override func next() -> XAttributeSpot? { 224 | if prefetched { 225 | prefetched = false 226 | } 227 | else { 228 | let next = attributeIterator.next() 229 | if keepLast && next == nil { 230 | return nil 231 | } 232 | else { 233 | current?.removeAttributeIterator(self) 234 | current = next 235 | current?.addAttributeIterator(self) 236 | } 237 | } 238 | if let value = current?.value, let element = current?.element { 239 | return XAttributeSpot(name: name, value: value, element: element) 240 | } 241 | else { 242 | current?.removeAttributeIterator(self) 243 | return nil 244 | } 245 | } 246 | 247 | public func previous() -> XAttributeSpot? { 248 | prefetched = false 249 | current?.removeAttributeIterator(self) 250 | current = attributeIterator.previous() 251 | if let value = current?.value, let element = current?.element { 252 | current?.addAttributeIterator(self) 253 | return XAttributeSpot(name: name, value:value, element: element) 254 | } 255 | else { 256 | return nil 257 | } 258 | } 259 | 260 | public func prefetch() { 261 | current?.removeAttributeIterator(self) 262 | current = attributeIterator.next() 263 | current?.addAttributeIterator(self) 264 | prefetched = true 265 | } 266 | } 267 | 268 | /** 269 | The XProcessingInstructionIterator does the work of making sure that an XML tree can be manipulated 270 | during iteration. It is ignorant about what precise iteration takes place. The 271 | precise iteration is implemented in "iteratorImplementation" which implements 272 | the XIteratorProtocol. 273 | */ 274 | public final class XBidirectionalProcessingInstructionIterator: XProcessingInstructionIterator { 275 | 276 | var previousIterator: XBidirectionalProcessingInstructionIterator? = nil 277 | var nextIterator: XBidirectionalProcessingInstructionIterator? = nil 278 | 279 | public typealias Element = XProcessingInstruction 280 | 281 | var processingInstructionIterator: XProcessingInstructionIteratorProtocol 282 | 283 | public init(processingInstructionIterator: XProcessingInstructionIteratorProtocol) { 284 | self.processingInstructionIterator = processingInstructionIterator } 285 | 286 | weak var current: XProcessingInstruction? = nil 287 | var prefetched = false 288 | 289 | public override func next() -> XProcessingInstruction? { 290 | if prefetched { 291 | prefetched = false 292 | return current 293 | } 294 | current?.removeProcessingInstructionIterator(self) 295 | current = processingInstructionIterator.next() 296 | current?.addProcessingInstructionIterator(self) 297 | return current 298 | } 299 | 300 | public override func previous() -> XProcessingInstruction? { 301 | prefetched = false 302 | current?.removeProcessingInstructionIterator(self) 303 | current = processingInstructionIterator.previous() 304 | current?.addProcessingInstructionIterator(self) 305 | return current 306 | } 307 | 308 | public func prefetch() { 309 | current?.removeProcessingInstructionIterator(self) 310 | current = processingInstructionIterator.next() 311 | current?.addProcessingInstructionIterator(self) 312 | prefetched = true 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /Tests/SwiftXMLTests/AttributeNamespacesTests.swift: -------------------------------------------------------------------------------- 1 | //===--- AttributeNamespacesTests.swift -----------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | //-the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | import XCTest 12 | import class Foundation.Bundle 13 | @testable import SwiftXML 14 | import SwiftXMLInterfaces 15 | 16 | final class AttributeNamespacesTests: XCTestCase { 17 | 18 | func testManual() throws { 19 | 20 | let element = XElement( 21 | "test", 22 | ["attribute1": "value1"], 23 | prefixed: [ 24 | "prefix1": [ 25 | "attribute1": "prefix1-attribute1", 26 | "attribute2": "prefix1-attribute2", 27 | ], 28 | "prefix2": [ 29 | "attribute1": "prefix2-attribute1" 30 | ] 31 | ] 32 | ) 33 | 34 | element[nil,"attribute2"] = "value2" 35 | 36 | XCTAssertEqual(element["attribute1"], "value1") 37 | XCTAssertEqual(element["attribute2"], "value2") 38 | XCTAssertEqual(element["prefix1","attribute1"], "prefix1-attribute1") 39 | XCTAssertEqual(element["prefix1","attribute2"], "prefix1-attribute2") 40 | XCTAssertEqual(element["prefix2","attribute1"], "prefix2-attribute1") 41 | XCTAssertEqual(element["prefix2","attribute2"], nil) 42 | XCTAssertEqual(element["prefix3","attribute1"], nil) 43 | 44 | XCTAssertEqual(element.attributeNames, ["attribute1", "attribute2"]) 45 | XCTAssertTrue(areEqual(element.attributeNamesWithPrefix, [(nil, "attribute1"), (nil, "attribute2"), ("prefix1", "attribute1"), ("prefix1", "attribute2"), ("prefix2", "attribute1")])) 46 | 47 | element[nil,"attribute1"] = nil 48 | element["prefix1","attribute2"] = nil 49 | element["prefix3","attribute1"] = "prefix3-attribute1" 50 | 51 | XCTAssertEqual(element["attribute1"], nil) 52 | XCTAssertEqual(element["attribute2"], "value2") 53 | XCTAssertEqual(element["prefix1","attribute1"], "prefix1-attribute1") 54 | XCTAssertEqual(element["prefix1","attribute2"], nil) 55 | XCTAssertEqual(element["prefix2","attribute1"], "prefix2-attribute1") 56 | XCTAssertEqual(element["prefix2","attribute2"], nil) 57 | XCTAssertEqual(element["prefix3","attribute1"], "prefix3-attribute1") 58 | 59 | XCTAssertEqual(element.description, #""#) 60 | XCTAssertEqual(element.serialized, #""#) 61 | 62 | let clone = element.clone 63 | XCTAssertEqual(clone.description, #""#) 64 | XCTAssertEqual(clone.serialized, #""#) 65 | } 66 | 67 | func testParsing() throws { 68 | 69 | let zNamespaceURI = "https://z" 70 | 71 | let source = """ 72 | 73 | 74 | 75 | 76 | """ 77 | 78 | let document = try parseXML(fromText: source, namespaceAware: true) 79 | 80 | let b = document.elements("b").first 81 | let c = document.elements("c").first 82 | 83 | XCTAssertNotNil(b) 84 | XCTAssertNotNil(c) 85 | let zPrefix = document.prefix(forNamespaceURI: zNamespaceURI) 86 | 87 | // b: 88 | XCTAssertEqual(b?["z:id"], nil) 89 | XCTAssertEqual(b?[zPrefix,"id"], "123") 90 | 91 | // c: 92 | XCTAssertEqual(c?["y:id"], "456") 93 | 94 | XCTAssertEqual(document.serialized, """ 95 | 96 | 97 | 98 | 99 | """) 100 | } 101 | 102 | func testParsingWithPrefixCorrection() throws { 103 | 104 | let z1NamespaceURI = "https://z1" 105 | let z2NamespaceURI = "https://z2" 106 | 107 | let source = """ 108 | 109 | 110 | 111 | """ 112 | 113 | let document = try parseXML(fromText: source, namespaceAware: true) 114 | 115 | XCTAssertEqual(document.serialized, """ 116 | 117 | 118 | 119 | """) 120 | } 121 | 122 | func testMovingIntoOtherDocument() throws { 123 | 124 | let namespaceURI1 = "https://z1" 125 | let namespaceURI2 = "https://z2" 126 | 127 | let source1 = """ 128 | 129 | 130 | 131 | """ 132 | 133 | let sourceWithSameNamespaceForPrefix = """ 134 | 135 | """ 136 | 137 | let sourceWithSameNamespaceButDifferentPrefix = """ 138 | 139 | """ 140 | 141 | let sourceWithDifferentNamespaceForSamePrefix = """ 142 | 143 | """ 144 | 145 | let document1 = try parseXML(fromText: source1, namespaceAware: true) 146 | let documentWithSameNamespaceForPrefix = try parseXML(fromText: sourceWithSameNamespaceForPrefix, namespaceAware: true) 147 | let documentWithSameNamespaceButDifferentPrefix = try parseXML(fromText: sourceWithSameNamespaceButDifferentPrefix, namespaceAware: true) 148 | let documentWithDifferentNamespaceForSamePrefix = try parseXML(fromText: sourceWithDifferentNamespaceForSamePrefix, namespaceAware: true) 149 | 150 | let b = document1.elements("b").first 151 | XCTAssertNotNil(b) 152 | 153 | b?.remove() // be sure it is also correctly done when first removing 154 | documentWithSameNamespaceForPrefix.firstChild?.add{ b?.clone } 155 | documentWithSameNamespaceButDifferentPrefix.firstChild?.add{ b?.clone } 156 | documentWithDifferentNamespaceForSamePrefix.firstChild?.add{ b?.clone } 157 | 158 | XCTAssertEqual(documentWithSameNamespaceForPrefix.serialized, """ 159 | 160 | """) 161 | XCTAssertEqual(documentWithSameNamespaceButDifferentPrefix.serialized, """ 162 | 163 | """) 164 | XCTAssertEqual(documentWithDifferentNamespaceForSamePrefix.serialized, """ 165 | 166 | """) 167 | } 168 | 169 | func testRegisteredAttributes() throws { 170 | 171 | let namespaceURI = "https://z1" 172 | 173 | let source = """ 174 | 175 | 176 | 177 | 178 | """ 179 | 180 | let document = try parseXML( 181 | fromText: source, 182 | namespaceAware: true, 183 | registeringAttributesForNamespaces: .selected([NamespaceURIAndName(namespaceURI: namespaceURI, name: "id")]) 184 | ) 185 | 186 | let prefix = document.prefix(forNamespaceURI: namespaceURI) 187 | XCTAssertNotNil(prefix) 188 | 189 | // cannot be found with the prefix: 190 | XCTAssertEqual( 191 | document.registeredAttributes("id").map{ $0.element.description }.joined(separator: "\n"), 192 | """ 193 | """ 194 | ) 195 | 196 | XCTAssertEqual( 197 | document.registeredAttributes(prefix: prefix, "id").map{ $0.element.description }.joined(separator: "\n"), 198 | """ 199 | 200 | 201 | """ 202 | ) 203 | 204 | let transformation = XTransformation { 205 | 206 | XRule(forRegisteredAttributes: "id") { attribute in 207 | attribute.element.add { "found attribute in the wrong way!" } // should not happen 208 | } 209 | 210 | XRule(forPrefix: prefix, forRegisteredAttributes: "id") { attribute in 211 | attribute.element.add { "found attribute!" } 212 | } 213 | 214 | } 215 | 216 | transformation.execute(inDocument: document) 217 | 218 | XCTAssertEqual( 219 | document.serialized, 220 | """ 221 | 222 | found attribute! 223 | found attribute! 224 | 225 | """ 226 | ) 227 | } 228 | 229 | func testRegisteredAttributeValues() throws { 230 | 231 | let namespaceURI = "http://z" 232 | 233 | let source = """ 234 | 235 | 236 | 237 | First reference to "1". 238 | Second reference to "1". 239 | 240 | """ 241 | 242 | let document = try parseXML( 243 | fromText: source, 244 | namespaceAware: true, 245 | registeringAttributeValuesForForNamespaces: .selected([ 246 | NamespaceURIAndName(namespaceURI: "http://z", name: "id"), 247 | NamespaceURIAndName(namespaceURI: "http://z", name: "refid") , 248 | ]) 249 | ) 250 | 251 | let prefix = document.prefix(forNamespaceURI: namespaceURI) 252 | 253 | // cannot find them by name only: 254 | XCTAssertEqual( 255 | document.registeredAttributes("id").map{ $0.element.description }.joined(separator: "\n"), 256 | """ 257 | """ 258 | ) 259 | 260 | // cannot find them without the prefix: 261 | XCTAssertEqual( 262 | document.registeredValues("1", forAttribute: "id").map{ $0.element.description }.joined(separator: "\n"), 263 | """ 264 | """ 265 | ) 266 | 267 | XCTAssertEqual( 268 | document.registeredValues("1", forAttribute: "id", withPrefix: prefix).map{ $0.element.serialized }.joined(separator: "\n"), 269 | """ 270 | 271 | """ 272 | ) 273 | 274 | XCTAssertEqual( 275 | document.registeredValues("1", forAttribute: "refid", withPrefix: prefix).map{ $0.element.serialized }.joined(separator: "\n"), 276 | """ 277 | First reference to "1". 278 | Second reference to "1". 279 | """ 280 | ) 281 | 282 | } 283 | 284 | } 285 | 286 | fileprivate func areEqual(_ array1: [(String?,String)], _ array2: [(String?,String)]) -> Bool { 287 | let size = array1.count 288 | guard size == array2.count else { return false } 289 | for i in 0.. URL?)? = nil, 36 | externalParsedEntityGetter: ((String) -> Data?)? = nil, 37 | externalWrapperElement: String? = nil, 38 | keepComments: Bool = false, 39 | keepCDATASections: Bool = false, 40 | eventHandlers: [XEventHandler]? = nil, 41 | immediateTextHandlingNearEntities: ImmediateTextHandlingNearEntities = .atExternalEntities 42 | ) throws -> XDocument { 43 | 44 | let document = XDocument() 45 | 46 | switch documentSource { 47 | case .url(url: let url): 48 | document._sourcePath = url.path 49 | case .path(path: let path): 50 | document._sourcePath = path 51 | default: 52 | break 53 | } 54 | 55 | let parser = ConvenienceParser( 56 | parser: XParser( 57 | internalEntityAutoResolve: internalEntityAutoResolve, 58 | internalEntityResolver: internalEntityResolver, 59 | internalEntityResolverHasToResolve: internalEntityResolverHasToResolve, 60 | textAllowedInElementWithName: textAllowedInElementWithName, 61 | insertExternalParsedEntities: insertExternalParsedEntities, 62 | externalParsedEntitySystemResolver: externalParsedEntitySystemResolver, 63 | externalParsedEntityGetter: externalParsedEntityGetter 64 | ), 65 | mainEventHandler: XParseBuilder( 66 | document: document, 67 | namespaceAware: namespaceAware, 68 | silentEmptyRootPrefix: silentEmptyRootPrefix, 69 | keepComments: keepComments, 70 | keepCDATASections: keepCDATASections, 71 | externalWrapperElement: externalWrapperElement, 72 | registeringAttributes: registeringAttributes, 73 | registeringAttributeValuesFor: registeringAttributeValuesFor, 74 | registeringAttributesForNamespaces: registeringAttributesForNamespaces, 75 | registeringAttributeValuesForForNamespaces: registeringAttributeValuesForForNamespaces 76 | ) 77 | ) 78 | 79 | try parser.parse(from: documentSource, sourceInfo: sourceInfo, eventHandlers: eventHandlers, immediateTextHandlingNearEntities: immediateTextHandlingNearEntities) 80 | 81 | return document 82 | } 83 | 84 | @available(*, deprecated, message: "this package is deprecated, use the repository https://github.com/swiftxml/SwiftXML instead and note the version number being reset to 1.0.0") 85 | public func parseXML( 86 | fromPath path: String, 87 | namespaceAware: Bool = false, 88 | silentEmptyRootPrefix: Bool = false, 89 | registeringAttributes: AttributeRegisterMode = .none, 90 | registeringAttributeValuesFor: AttributeRegisterMode = .none, 91 | registeringAttributesForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 92 | registeringAttributeValuesForForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 93 | sourceInfo: String? = nil, 94 | textAllowedInElementWithName: [String]? = nil, 95 | internalEntityAutoResolve: Bool = false, 96 | internalEntityResolver: InternalEntityResolver? = nil, 97 | internalEntityResolverHasToResolve: Bool = true, 98 | insertExternalParsedEntities: Bool = false, 99 | externalParsedEntitySystemResolver: ((String) -> URL?)? = nil, 100 | externalParsedEntityGetter: ((String) -> Data?)? = nil, 101 | externalWrapperElement: String? = nil, 102 | keepComments: Bool = false, 103 | keepCDATASections: Bool = false, 104 | eventHandlers: [XEventHandler]? = nil, 105 | immediateTextHandlingNearEntities: ImmediateTextHandlingNearEntities = .atExternalEntities 106 | ) throws -> XDocument { 107 | try parseXML( 108 | from: .path(path), 109 | namespaceAware: namespaceAware, 110 | silentEmptyRootPrefix: silentEmptyRootPrefix, 111 | registeringAttributes: registeringAttributes, 112 | registeringAttributeValuesFor: registeringAttributeValuesFor, 113 | registeringAttributesForNamespaces: registeringAttributesForNamespaces, 114 | registeringAttributeValuesForForNamespaces: registeringAttributeValuesForForNamespaces, 115 | sourceInfo: sourceInfo, 116 | textAllowedInElementWithName: textAllowedInElementWithName, 117 | internalEntityAutoResolve: internalEntityAutoResolve, 118 | internalEntityResolver: internalEntityResolver, 119 | internalEntityResolverHasToResolve: internalEntityResolverHasToResolve, 120 | insertExternalParsedEntities: insertExternalParsedEntities, 121 | externalParsedEntitySystemResolver: externalParsedEntitySystemResolver, 122 | externalParsedEntityGetter: externalParsedEntityGetter, 123 | externalWrapperElement: externalWrapperElement, 124 | keepComments: keepComments, 125 | keepCDATASections: keepCDATASections, 126 | eventHandlers: eventHandlers, 127 | immediateTextHandlingNearEntities: immediateTextHandlingNearEntities 128 | ) 129 | } 130 | 131 | @available(*, deprecated, message: "this package is deprecated, use the repository https://github.com/swiftxml/SwiftXML instead and note the version number being reset to 1.0.0") 132 | public func parseXML( 133 | fromURL url: URL, 134 | namespaceAware: Bool = false, 135 | silentEmptyRootPrefix: Bool = false, 136 | registeringAttributes: AttributeRegisterMode = .none, 137 | registeringAttributeValuesFor: AttributeRegisterMode = .none, 138 | registeringAttributesForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 139 | registeringAttributeValuesForForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 140 | sourceInfo: String? = nil, 141 | textAllowedInElementWithName: [String]? = nil, 142 | internalEntityAutoResolve: Bool = false, 143 | internalEntityResolver: InternalEntityResolver? = nil, 144 | internalEntityResolverHasToResolve: Bool = true, 145 | insertExternalParsedEntities: Bool = false, 146 | externalParsedEntitySystemResolver: ((String) -> URL?)? = nil, 147 | externalParsedEntityGetter: ((String) -> Data?)? = nil, 148 | externalWrapperElement: String? = nil, 149 | keepComments: Bool = false, 150 | keepCDATASections: Bool = false, 151 | eventHandlers: [XEventHandler]? = nil, 152 | immediateTextHandlingNearEntities: ImmediateTextHandlingNearEntities = .atExternalEntities 153 | ) throws -> XDocument { 154 | try parseXML( 155 | from: .url(url), 156 | namespaceAware: namespaceAware, 157 | silentEmptyRootPrefix: silentEmptyRootPrefix, 158 | registeringAttributes: registeringAttributes, 159 | registeringAttributeValuesFor: registeringAttributeValuesFor, 160 | registeringAttributesForNamespaces: registeringAttributesForNamespaces, 161 | registeringAttributeValuesForForNamespaces: registeringAttributeValuesForForNamespaces, 162 | sourceInfo: sourceInfo, 163 | textAllowedInElementWithName: textAllowedInElementWithName, 164 | internalEntityAutoResolve: internalEntityAutoResolve, 165 | internalEntityResolver: internalEntityResolver, 166 | internalEntityResolverHasToResolve: internalEntityResolverHasToResolve, 167 | insertExternalParsedEntities: insertExternalParsedEntities, 168 | externalParsedEntitySystemResolver: externalParsedEntitySystemResolver, 169 | externalParsedEntityGetter: externalParsedEntityGetter, 170 | externalWrapperElement: externalWrapperElement, 171 | keepComments: keepComments, 172 | keepCDATASections: keepCDATASections, 173 | eventHandlers: eventHandlers, 174 | immediateTextHandlingNearEntities: immediateTextHandlingNearEntities 175 | ) 176 | } 177 | 178 | @available(*, deprecated, message: "this package is deprecated, use the repository https://github.com/swiftxml/SwiftXML instead and note the version number being reset to 1.0.0") 179 | public func parseXML( 180 | fromText text: String, 181 | namespaceAware: Bool = false, 182 | silentEmptyRootPrefix: Bool = false, 183 | registeringAttributes: AttributeRegisterMode = .none, 184 | registeringAttributeValuesFor: AttributeRegisterMode = .none, 185 | registeringAttributesForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 186 | registeringAttributeValuesForForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 187 | sourceInfo: String? = nil, 188 | textAllowedInElementWithName: [String]? = nil, 189 | internalEntityAutoResolve: Bool = false, 190 | internalEntityResolver: InternalEntityResolver? = nil, 191 | internalEntityResolverHasToResolve: Bool = true, 192 | insertExternalParsedEntities: Bool = false, 193 | externalParsedEntitySystemResolver: ((String) -> URL?)? = nil, 194 | externalParsedEntityGetter: ((String) -> Data?)? = nil, 195 | externalWrapperElement: String? = nil, 196 | keepComments: Bool = false, 197 | keepCDATASections: Bool = false, 198 | eventHandlers: [XEventHandler]? = nil, 199 | immediateTextHandlingNearEntities: ImmediateTextHandlingNearEntities = .atExternalEntities 200 | ) throws -> XDocument { 201 | try parseXML( 202 | from: .text(text), 203 | namespaceAware: namespaceAware, 204 | silentEmptyRootPrefix: silentEmptyRootPrefix, 205 | registeringAttributes: registeringAttributes, 206 | registeringAttributeValuesFor: registeringAttributeValuesFor, 207 | registeringAttributesForNamespaces: registeringAttributesForNamespaces, 208 | registeringAttributeValuesForForNamespaces: registeringAttributeValuesForForNamespaces, 209 | sourceInfo: sourceInfo, 210 | textAllowedInElementWithName: textAllowedInElementWithName, 211 | internalEntityAutoResolve: internalEntityAutoResolve, 212 | internalEntityResolver: internalEntityResolver, 213 | internalEntityResolverHasToResolve: internalEntityResolverHasToResolve, 214 | insertExternalParsedEntities: insertExternalParsedEntities, 215 | externalParsedEntitySystemResolver: externalParsedEntitySystemResolver, 216 | externalParsedEntityGetter: externalParsedEntityGetter, 217 | externalWrapperElement: externalWrapperElement, 218 | keepComments: keepComments, 219 | keepCDATASections: keepCDATASections, 220 | eventHandlers: eventHandlers, 221 | immediateTextHandlingNearEntities: immediateTextHandlingNearEntities 222 | ) 223 | } 224 | 225 | @available(*, deprecated, message: "this package is deprecated, use the repository https://github.com/swiftxml/SwiftXML instead and note the version number being reset to 1.0.0") 226 | public func parseXML( 227 | fromData data: Data, 228 | namespaceAware: Bool = false, 229 | silentEmptyRootPrefix: Bool = false, 230 | registeringAttributes: AttributeRegisterMode = .none, 231 | registeringAttributeValuesFor: AttributeRegisterMode = .none, 232 | registeringAttributesForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 233 | registeringAttributeValuesForForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 234 | sourceInfo: String? = nil, 235 | internalEntityAutoResolve: Bool = false, 236 | internalEntityResolver: InternalEntityResolver? = nil, 237 | internalEntityResolverHasToResolve: Bool = true, 238 | eventHandlers: [XEventHandler]? = nil, 239 | immediateTextHandlingNearEntities: ImmediateTextHandlingNearEntities = .atExternalEntities, 240 | textAllowedInElementWithName: [String]? = nil, 241 | keepComments: Bool = false, 242 | keepCDATASections: Bool = false, 243 | insertExternalParsedEntities: Bool = false, 244 | externalParsedEntitySystemResolver: ((String) -> URL?)? = nil, 245 | externalParsedEntityGetter: ((String) -> Data?)? = nil, 246 | externalWrapperElement: String? = nil, 247 | externalWrapperNameAttribute: String? = nil, 248 | externalWrapperPathAttribute: String? = nil 249 | ) throws -> XDocument { 250 | try parseXML( 251 | from: .data(data), 252 | namespaceAware: namespaceAware, 253 | silentEmptyRootPrefix: silentEmptyRootPrefix, 254 | registeringAttributes: registeringAttributes, 255 | registeringAttributeValuesFor: registeringAttributeValuesFor, 256 | registeringAttributesForNamespaces: registeringAttributesForNamespaces, 257 | registeringAttributeValuesForForNamespaces: registeringAttributeValuesForForNamespaces, 258 | sourceInfo: sourceInfo, 259 | textAllowedInElementWithName: textAllowedInElementWithName, 260 | internalEntityAutoResolve: internalEntityAutoResolve, 261 | internalEntityResolver: internalEntityResolver, 262 | internalEntityResolverHasToResolve: internalEntityResolverHasToResolve, 263 | insertExternalParsedEntities: insertExternalParsedEntities, 264 | externalParsedEntitySystemResolver: externalParsedEntitySystemResolver, 265 | externalParsedEntityGetter: externalParsedEntityGetter, 266 | externalWrapperElement: externalWrapperElement, 267 | keepComments: keepComments, 268 | keepCDATASections: keepCDATASections, 269 | eventHandlers: eventHandlers, 270 | immediateTextHandlingNearEntities: immediateTextHandlingNearEntities 271 | ) 272 | } 273 | -------------------------------------------------------------------------------- /Sources/SwiftXML/Utilities.swift: -------------------------------------------------------------------------------- 1 | //===--- Utilities.swift --------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | #if canImport(FoundationEssentials) 12 | import FoundationEssentials 13 | #else 14 | import Foundation 15 | #endif 16 | 17 | public struct SwiftXMLError: LocalizedError, CustomStringConvertible { 18 | 19 | private let message: String 20 | 21 | public init(_ message: String) { 22 | self.message = message 23 | } 24 | 25 | public var description: String { message } 26 | 27 | public var errorDescription: String? { message } 28 | } 29 | 30 | public extension String { 31 | 32 | var escapingAllForXML: String { 33 | self 34 | .replacing("&", with: "&") 35 | .replacing("<", with: "<") 36 | .replacing(">", with: ">") 37 | .replacing("\"", with: """) 38 | .replacing("'", with: "'") 39 | } 40 | 41 | var escapingForXML: String { 42 | self 43 | .replacing("&", with: "&") 44 | .replacing("<", with: "<") 45 | } 46 | 47 | var escapingDoubleQuotedValueForXML: String { 48 | self 49 | .replacing("&", with: "&") 50 | .replacing("<", with: "<") 51 | .replacing("\"", with: """) 52 | } 53 | 54 | var escapingSimpleQuotedValueForXML: String { 55 | self 56 | .replacing("&", with: "&") 57 | .replacing("<", with: "<") 58 | .replacing("'", with: "'") 59 | } 60 | 61 | } 62 | 63 | extension String { 64 | 65 | func appending(_ string: String?) -> String { 66 | if let string { self + string } else { self } 67 | } 68 | 69 | func prepending(_ string: String?) -> String { 70 | if let string { string + self } else { self } 71 | } 72 | 73 | } 74 | 75 | public func sortByName(_ declarations: [String:XDeclarationInInternalSubset]) -> [XDeclarationInInternalSubset] { 76 | var sorted = [XDeclarationInInternalSubset]() 77 | for name in declarations.keys.sorted() { 78 | if let theDeclaration = declarations[name] { 79 | sorted.append(theDeclaration) 80 | } 81 | } 82 | return sorted 83 | } 84 | 85 | struct Stack { 86 | var elements = [Element]() 87 | mutating func push(_ item: Element) { 88 | elements.append(item) 89 | } 90 | mutating func change(_ item: Element) { 91 | _ = pop() 92 | elements.append(item) 93 | } 94 | mutating func pop() -> Element? { 95 | if elements.isEmpty { 96 | return nil 97 | } 98 | else { 99 | return elements.removeLast() 100 | } 101 | } 102 | func peek() -> Element? { 103 | return elements.last 104 | } 105 | func peekAll() -> [Element] { 106 | return elements 107 | } 108 | } 109 | 110 | public final class WeaklyListed { 111 | var next: WeaklyListed? = nil 112 | 113 | weak var element: T? 114 | 115 | init(_ element: T) { 116 | self.element = element 117 | } 118 | 119 | // prevent stack overflow when destroying the list, 120 | // to be applied on the first element in that list, 121 | // cf. https://forums.swift.org/t/deep-recursion-in-deinit-should-not-happen/54987 122 | // !!! This should not be necessary anymore with Swift 5.7 or on masOS 13. !!! 123 | func removeFollowing() { 124 | var node = self 125 | while isKnownUniquelyReferenced(&node.next) { 126 | (node, node.next) = (node.next!, nil) 127 | } 128 | } 129 | } 130 | 131 | /** 132 | A list that stores its elements weakly. It looks for zombies whenever operating 133 | on it; therefore it is only suitable for a small number of elements. 134 | */ 135 | public final class WeakList: LazySequenceProtocol { 136 | 137 | var first: WeaklyListed? = nil 138 | 139 | public func remove(_ o: T) { 140 | var previous: WeaklyListed? = nil 141 | var iterated = first 142 | while let item = iterated { 143 | if item.element == nil || item.element === o { 144 | previous?.next = item.next 145 | item.next = nil 146 | if item === first { 147 | first = nil 148 | } 149 | } 150 | previous = iterated 151 | iterated = item.next 152 | } 153 | } 154 | 155 | public func append(_ o: T) { 156 | if first == nil { 157 | first = WeaklyListed(o) 158 | } 159 | else { 160 | var previous: WeaklyListed? = nil 161 | var iterated = first 162 | while let item = iterated { 163 | if item.element == nil || item.element === o { 164 | previous?.next = item.next 165 | item.next = nil 166 | } 167 | previous = iterated 168 | iterated = item.next 169 | if iterated == nil { 170 | previous?.next = WeaklyListed(o) 171 | } 172 | } 173 | } 174 | } 175 | 176 | public func makeIterator() -> WeakListIterator { 177 | return WeakListIterator(start: first) 178 | } 179 | 180 | deinit { 181 | first?.removeFollowing() 182 | } 183 | } 184 | 185 | public final class WeakListIterator: IteratorProtocol { 186 | 187 | var started = false 188 | var current: WeaklyListed? 189 | 190 | public init(start: WeaklyListed?) { 191 | current = start 192 | } 193 | 194 | public func next() -> T? { 195 | var previous: WeaklyListed? = nil 196 | if started { 197 | previous = current 198 | current = current?.next 199 | } 200 | else { 201 | started = true 202 | } 203 | while let item = current, item.element == nil { 204 | previous?.next = item.next 205 | current = item.next 206 | item.next = nil 207 | } 208 | return current?.element 209 | } 210 | } 211 | 212 | public struct SimplePropertiesParseError: LocalizedError { 213 | 214 | private let message: String 215 | 216 | init(_ message: String) { 217 | self.message = message 218 | } 219 | 220 | public var errorDescription: String? { 221 | return message 222 | } 223 | } 224 | 225 | func escapeInSimplePropertiesList(_ text: String) -> String { 226 | return text 227 | .replacing("\\", with: "\\\\") 228 | .replacing("=", with: "\\=") 229 | .replacing(":", with: "\\:") 230 | .replacing("\n", with: "\\n") 231 | .replacing("#", with: "\\#") 232 | } 233 | func unescapeInSimplePropertiesList(_ text: String) -> String { 234 | return text.components(separatedBy: "\\\\").map { $0 235 | .replacing("\\#", with: "#") 236 | .replacing("\\n", with: "\n") 237 | .replacing("\\:", with: ":") 238 | .replacing("\\=", with: "=") 239 | .replacing("\\\\", with: "\\") 240 | }.joined(separator: "\\") 241 | } 242 | 243 | extension Sequence { 244 | 245 | func forEachAsync ( 246 | _ operation: (Element) async throws -> Void 247 | ) async rethrows { 248 | for element in self { 249 | try await operation(element) 250 | } 251 | } 252 | } 253 | 254 | extension Array where Element == String? { 255 | 256 | func joined(separator: String) -> String? { 257 | var nonNils = [String]() 258 | for s in self { 259 | if let s = s { 260 | nonNils.append(s) 261 | } 262 | } 263 | return nonNils.isEmpty ? nil : nonNils.joined(separator: separator) 264 | } 265 | 266 | func joinedNonEmpties(separator: String) -> String? { 267 | var nonEmpties = [String]() 268 | for s in self { 269 | if let s = s, !s.isEmpty { 270 | nonEmpties.append(s) 271 | } 272 | } 273 | return nonEmpties.isEmpty ? nil : nonEmpties.joined(separator: separator) 274 | } 275 | } 276 | 277 | extension String { 278 | 279 | var nonEmpty: String? { self.isEmpty ? nil : self } 280 | 281 | var avoidingDoubleHyphens: String { 282 | 283 | var result = if self.contains("--") { 284 | self.replacing("--", with: "(HYPHEN)(HYPHEN)") 285 | } else { 286 | self 287 | } 288 | 289 | if result.hasPrefix("-") { 290 | result = "(HYPHEN)\(result.dropFirst())" 291 | } 292 | 293 | if result.hasSuffix("-") { 294 | result = "\(result.dropLast())(HYPHEN)" 295 | } 296 | 297 | return result 298 | } 299 | 300 | } 301 | 302 | /// A wrapper around Set that can passed around by reference. 303 | class Referenced { 304 | 305 | var referenced: T 306 | 307 | init(_ referenced: T) { 308 | self.referenced = referenced 309 | } 310 | 311 | } 312 | 313 | class TwoTieredDictionaryWithStringKeys { 314 | 315 | var dictionary = [String:Referenced<[String:V]>]() 316 | 317 | init() {} 318 | 319 | var isEmpty: Bool { dictionary.isEmpty } 320 | 321 | func put(key1: String, key2: String, value: V?) { 322 | let indexForKey2 = dictionary[key1] ?? { 323 | let newIndex = Referenced([String:V]()) 324 | dictionary[key1] = newIndex 325 | return newIndex 326 | }() 327 | indexForKey2.referenced[key2] = value 328 | if indexForKey2.referenced.isEmpty { 329 | dictionary[key1] = nil 330 | } 331 | } 332 | 333 | func removeValue(forKey1 key1: String, andKey2 key2: String) -> V? { 334 | guard let indexForKey2 = dictionary[key1] else { return nil } 335 | guard let value = indexForKey2.referenced[key2] else { return nil } 336 | indexForKey2.referenced[key2] = nil 337 | if indexForKey2.referenced.isEmpty { 338 | dictionary[key1] = nil 339 | } 340 | return value 341 | } 342 | 343 | subscript(key1: String, key2: String) -> V? { 344 | 345 | set { 346 | put(key1: key1, key2: key2, value: newValue) 347 | } 348 | 349 | get { 350 | return dictionary[key1]?.referenced[key2] 351 | } 352 | 353 | } 354 | 355 | subscript(key1: String) -> [String:V]? { 356 | 357 | get { 358 | return dictionary[key1]?.referenced 359 | } 360 | 361 | } 362 | 363 | var firstKeys: Dictionary>>.Keys { dictionary.keys } 364 | 365 | func secondKeys(forLeftKey leftKey: String) -> Dictionary.Keys? { 366 | return dictionary[leftKey]?.referenced.keys 367 | } 368 | 369 | var secondKeys: Set { 370 | var keys = Set() 371 | firstKeys.forEach { leftKey in 372 | secondKeys(forLeftKey: leftKey)?.forEach { rightKey in 373 | keys.insert(rightKey) 374 | } 375 | } 376 | return keys 377 | } 378 | 379 | var values: [V] { 380 | firstKeys.compactMap { dictionary[$0]?.referenced.values }.flatMap{ $0 } 381 | } 382 | 383 | func values(forFirstKey key1: String) -> Dictionary.Values? { 384 | dictionary[key1]?.referenced.values 385 | } 386 | 387 | func removeAll(keepingCapacity keepCapacity: Bool = false) { 388 | dictionary.removeAll(keepingCapacity: keepCapacity) 389 | } 390 | 391 | var keys: [(String,String)] { 392 | firstKeys.flatMap{ key1 in 393 | dictionary[key1]!.referenced.keys.map{ (key1, $0) } 394 | } 395 | } 396 | 397 | var all: [(String,String,V)] { 398 | firstKeys.flatMap{ key1 in 399 | dictionary[key1]!.referenced.map{ (key1, $0.key, $0.value) } 400 | } 401 | } 402 | 403 | var sorted: [(String,String,V)] { 404 | firstKeys.sorted().flatMap{ key1 in 405 | dictionary[key1]!.referenced.sorted(by: { $0.key < $1.key }).map{ (key1, $0.key, $0.value) } 406 | } 407 | } 408 | 409 | } 410 | 411 | class ThreeTieredDictionaryWithStringKeys { 412 | 413 | var dictionary = [String:Referenced<[String:Referenced<[String:V]>]>]() 414 | 415 | init() {} 416 | 417 | var isEmpty: Bool { dictionary.isEmpty } 418 | 419 | func put(key1: String, key2: String, key3: String, value: V?) { 420 | let indexForKey2 = dictionary[key1] ?? { 421 | let newIndexForKey2 = Referenced([String:Referenced<[String:V]>]()) 422 | dictionary[key1] = newIndexForKey2 423 | return newIndexForKey2 424 | }() 425 | let indexForKey3 = indexForKey2.referenced[key2] ?? { 426 | let newIndexForKey3 = Referenced([String:V]()) 427 | indexForKey2.referenced[key2] = newIndexForKey3 428 | return newIndexForKey3 429 | }() 430 | indexForKey3.referenced[key3] = value 431 | if indexForKey3.referenced.isEmpty { 432 | dictionary[key1]?.referenced[key2] = nil 433 | if dictionary[key1]?.referenced.isEmpty == true { 434 | dictionary[key1] = nil 435 | } 436 | } 437 | } 438 | 439 | func removeValue(forKey1 key1: String, andKey2 key2: String, andKey3 key3: String) -> V? { 440 | guard let indexForKey2 = dictionary[key1] else { return nil } 441 | guard let indexForKey3 = indexForKey2.referenced[key2] else { return nil } 442 | guard let value = indexForKey3.referenced[key2] else { return nil } 443 | indexForKey3.referenced[key3] = nil 444 | if indexForKey3.referenced.isEmpty { 445 | indexForKey2.referenced[key2] = nil 446 | if indexForKey2.referenced.isEmpty { 447 | dictionary[key1] = nil 448 | } 449 | } 450 | return value 451 | } 452 | 453 | subscript(key1: String, key2: String, key3: String) -> V? { 454 | 455 | set { 456 | put(key1: key1, key2: key2, key3: key3, value: newValue) 457 | } 458 | 459 | get { 460 | return dictionary[key1]?.referenced[key2]?.referenced[key3] 461 | } 462 | 463 | } 464 | 465 | subscript(key1: String, key2: String) -> [String:V]? { 466 | 467 | get { 468 | return dictionary[key1]?.referenced[key2]?.referenced 469 | } 470 | 471 | } 472 | 473 | var firstKeys: Dictionary]>>.Keys { dictionary.keys } 474 | 475 | func removeAll(keepingCapacity keepCapacity: Bool = false) { 476 | dictionary.removeAll(keepingCapacity: keepCapacity) 477 | } 478 | 479 | var keys: [(String,String,String)] { 480 | firstKeys.flatMap{ key1 in 481 | dictionary[key1]!.referenced.flatMap{ (key2,indexForKey3) in 482 | indexForKey3.referenced.keys.map{ (key3) in 483 | (key1, key2, key3) 484 | } 485 | } 486 | } 487 | } 488 | 489 | var all: [(String,String,String,V)] { 490 | firstKeys.flatMap{ key1 in 491 | dictionary[key1]!.referenced.flatMap{ (key2,indexForKey3) in 492 | indexForKey3.referenced.map{ (key3,value) in 493 | (key1, key2, key3, value) 494 | } 495 | } 496 | } 497 | } 498 | 499 | var sorted: [(String,String,String,V)] { 500 | firstKeys.sorted().flatMap{ key1 in 501 | dictionary[key1]!.referenced.sorted(by: { $0.key < $1.key }).flatMap{ (key2,indexForKey3) in 502 | indexForKey3.referenced.sorted(by: { $0.key < $1.key }).map{ (key3,value) in 503 | (key1, key2, key3, value) 504 | } 505 | } 506 | } 507 | } 508 | 509 | } 510 | -------------------------------------------------------------------------------- /Tests/SwiftXMLTests/ToolsTests.swift: -------------------------------------------------------------------------------- 1 | //===--- ToolsTests.swift -------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | import XCTest 12 | import class Foundation.Bundle 13 | import SwiftXMLInterfaces 14 | import SwiftXML 15 | 16 | final class ToolsTests: XCTestCase { 17 | 18 | func testCopyXStructure1() throws { 19 | let document = try parseXML(fromText: """ 20 | 21 | 22 | Typ „alphabetisch“ (alphabetic) 23 |

Das folgende ist eine Aufzählung:aAnleitung A

Das ist zu tun. 27 | ...usw. ...;

bAnleitung B

Und noch anderes. 29 | ...usw. ...

30 |
31 | """) 32 | 33 | let start = document.descendants("sec").first?.firstChild("p")?.allTexts.first 34 | let end = document.descendants("sec").first?.firstChild("p")?.descendants("term").first?.allTexts.dropFirst().first! 35 | 36 | XCTAssertEqual(start?.serialized(), #""" 37 | Das folgende ist eine Aufzählung: 38 | """#) 39 | XCTAssertEqual(end?.serialized(), #""" 40 | Anleitung A 41 | """#) 42 | 43 | let copyOfStructure = copyXStructure(from: start!, to: end!, upTo: start!.ancestors({ $0.name == "sec" }).first! )?.content 44 | XCTAssertEqual(copyOfStructure?.map{ $0.serialized() }.joined(), #""" 45 |

Das folgende ist eine Aufzählung:aAnleitung A

46 | """#) 47 | } 48 | 49 | func testCopyXStructure2() throws { 50 | let document = try parseXML(fromText: """ 51 | 52 | 53 | Typ „alphabetisch“ (alphabetic) 54 |

Das folgende ist eine Aufzählung:aVorgang für Fall A

Das ist zu tun. 58 | ... usw. ...;

bVorgang für Fall B

Das ist dann zu tun. 60 | ... usw. ...

61 |
62 | """) 63 | 64 | let start = document.descendants("sec").first?.children("p").first?.descendants("p").first?.allTexts.first 65 | let end = start 66 | 67 | XCTAssertEqual(start?.serialized().replacing(/\s+/, with: " ").trimming(), #""" 68 | Das ist zu tun. 69 | """#) 70 | XCTAssertEqual(end?.serialized().replacing(/\s+/, with: " ").trimming(), #""" 71 | Das ist zu tun. 72 | """#) 73 | 74 | let copyOfStructure = copyXStructure(from: start!, to: end!, upTo: start!.ancestors({ $0.name == "sec" }).first!)?.content 75 | XCTAssertEqual(copyOfStructure?.map{ $0.serialized() }.joined(), #""" 76 |

Das ist zu tun.

77 | """#) 78 | } 79 | 80 | func testCopyXStructure3() throws { 81 | let document = try parseXML(fromText: """ 82 | 83 | 84 | Der Titel 85 |

Das folgende

86 |

ist

87 |

eine Aufzählung:aAnleitung A

Das ist zu tun. 91 | ... usw. ...;

bAnleitung B

Das ist dann zu tun. 93 | ... usw. ....

94 |
95 | """) 96 | 97 | let start = document.descendants("sec").first?.firstChild("p")?.allTexts.first 98 | let end = document.descendants("sec").first?.children("p").dropFirst(2).first?.descendants("term").first?.allTexts.dropFirst().first! 99 | 100 | XCTAssertEqual(start?.serialized(), #""" 101 | Das folgende 102 | """#) 103 | XCTAssertEqual(end?.serialized(), #""" 104 | Anleitung A 105 | """#) 106 | 107 | let copyOfStructure = copyXStructure(from: start!, to: end!, upTo: start!.ancestors({ $0.name == "sec" }).first!)?.content 108 | 109 | XCTAssertEqual(copyOfStructure?.map{ $0.serialized() }.joined(), #""" 110 |

Das folgende

111 |

ist

112 |

eine Aufzählung:aAnleitung A

113 | """#) 114 | } 115 | 116 | func testCopyXStructure4() throws { 117 | let document = try parseXML(fromText: """ 118 |

In Abschnitt 1 und Abschnitt 2 muss man schauen.

119 | """) 120 | 121 | let start = document.allTexts.first 122 | let end = document.allTexts.dropFirst(4).first 123 | 124 | XCTAssertEqual(start?.serialized().replacing(/\s+/, with: " ").trimming(), #""" 125 | In 126 | """#) 127 | XCTAssertEqual(end?.serialized().replacing(/\s+/, with: " ").trimming(), #""" 128 | muss man schauen. 129 | """#) 130 | 131 | let copyOfStructure = copyXStructure(from: start!, to: end!, upTo: document.firstChild!) 132 | XCTAssertEqual(copyOfStructure?.serialized(), #""" 133 |

In Abschnitt 1 und Abschnitt 2 muss man schauen.

134 | """#) 135 | } 136 | 137 | func testCopyXStructure5() throws { 138 | let document = try parseXML(fromText: """ 139 |
140 |

Ja,

141 |

das ist so 1 %, echt.

142 |
143 | """, textAllowedInElementWithName: ["p", "span"]) 144 | 145 | let start = document.firstChild?.children.first?.allTexts.first 146 | let end = document.firstChild?.children.dropFirst().first?.allTexts.dropFirst(2).first 147 | 148 | XCTAssertEqual(start?.serialized().replacing(/\s+/, with: " ").trimming(), #""" 149 | Ja, 150 | """#) 151 | XCTAssertEqual(end?.serialized().replacing(/\s+/, with: " ").trimming(), #""" 152 | , echt. 153 | """#) 154 | 155 | let copyOfStructure = copyXStructure(from: start!, to: end!, upTo: document.firstChild!) 156 | XCTAssertEqual(copyOfStructure?.serialized(pretty: true, indentation: " "), #""" 157 |
158 |

Ja,

159 |

das ist so 1 %, echt.

160 |
161 | """#) 162 | } 163 | 164 | func testHTMLOutput0() throws { 165 | let source = """ 166 |

The title

1st paragraph

2nd paragraph

179 |
180 | """ 181 | ) 182 | } 183 | 184 | func testHTMLOutput1() throws { 185 | let source = """ 186 |
187 | """ 188 | XCTAssertEqual( 189 | try parseXML(fromText: source).serialized(usingProductionTemplate: HTMLProductionTemplate()), 190 | """ 191 | 192 |
193 | """ 194 | ) 195 | } 196 | 197 | func testHTMLOutput2() throws { 198 | let source = """ 199 |
200 | """ 201 | XCTAssertEqual( 202 | try parseXML(fromText: source).serialized(usingProductionTemplate: HTMLProductionTemplate()), 203 | """ 204 | 205 |
206 | """ 207 | ) 208 | } 209 | 210 | func testHTMLOutput3() throws { 211 | let source = """ 212 |

213 | """ 214 | XCTAssertEqual( 215 | try parseXML(fromText: source).serialized(usingProductionTemplate: HTMLProductionTemplate()), 216 | """ 217 | 218 |
219 | 220 |

221 |
222 | """ 223 | ) 224 | } 225 | 226 | func testHTMLOutput4() throws { 227 | let source = """ 228 |

229 | """ 230 | XCTAssertEqual( 231 | try parseXML(fromText: source).serialized( 232 | usingProductionTemplate: HTMLProductionTemplate() 233 | ), 234 | """ 235 | 236 |
237 | 238 | 239 |

240 |
241 | """ 242 | ) 243 | } 244 | 245 | func testHTMLOutput5() throws { 246 | let source = """ 247 |

248 | """ 249 | XCTAssertEqual( 250 | try parseXML(fromText: source).serialized( 251 | usingProductionTemplate: HTMLProductionTemplate( 252 | suppressUncessaryPrettyPrintAtAnchors: true 253 | ) 254 | ), 255 | """ 256 | 257 |
258 |

259 |
260 | """ 261 | ) 262 | } 263 | 264 | func testHTMLOutput6() throws { 265 | let source = """ 266 |
267 | """ 268 | XCTAssertEqual( 269 | try parseXML(fromText: source).serialized( 270 | usingProductionTemplate: HTMLProductionTemplate( 271 | suppressUncessaryPrettyPrintAtAnchors: true 272 | ) 273 | ), 274 | """ 275 | 276 |
277 | """ 278 | ) 279 | } 280 | 281 | func testHTMLOutput7() throws { 282 | let source = """ 283 |
Hello
world
!
284 | """ 285 | XCTAssertEqual( 286 | try parseXML(fromText: source).serialized( 287 | usingProductionTemplate: HTMLProductionTemplate( 288 | suppressUncessaryPrettyPrintAtAnchors: true 289 | ) 290 | ), 291 | """ 292 | 293 |
Hello 294 |
world
295 |
!
296 |
297 | """ 298 | ) 299 | } 300 | 301 | func testHTMLOutput8() throws { 302 | let source = """ 303 |
1
304 | """ 305 | XCTAssertEqual( 306 | try parseXML(fromText: source).serialized( 307 | usingProductionTemplate: HTMLProductionTemplate( 308 | suppressUncessaryPrettyPrintAtAnchors: true 309 | ) 310 | ), 311 | """ 312 | 313 |
314 |
315 | 316 | 317 | 318 | 319 |
1
320 |
321 |
322 | """ 323 | ) 324 | } 325 | 326 | func testHTMLOutput9() throws { 327 | let source = """ 328 |
Hello
1
329 | """ 330 | XCTAssertEqual( 331 | try parseXML(fromText: source).serialized( 332 | usingProductionTemplate: HTMLProductionTemplate( 333 | suppressUncessaryPrettyPrintAtAnchors: true 334 | ) 335 | ), 336 | """ 337 | 338 |
Hello 339 |
340 | 341 | 342 | 343 | 344 |
1
345 |
346 |
347 | """ 348 | ) 349 | } 350 | 351 | func testHTMLOutput10() throws { 352 | let source = """ 353 |
leading span of the wrapper block
a block within the block
followed by text an anchorand a text and some more text and another span
and another div
354 | """ 355 | XCTAssertEqual( 356 | try parseXML(fromText: source).serialized( 357 | usingProductionTemplate: HTMLProductionTemplate( 358 | suppressUncessaryPrettyPrintAtAnchors: true 359 | ) 360 | ), 361 | """ 362 | 363 |
leading span of the wrapper block 364 |
a block within the block
followed by text an anchorand a text and some more text and another span 365 |
and another div
366 |
367 | """ 368 | ) 369 | } 370 | 371 | func testHTMLOutput11() throws { 372 | let source = """ 373 |
Picture missing. No alternative text available.
374 | """ 375 | XCTAssertEqual( 376 | try parseXML(fromText: source).serialized( 377 | usingProductionTemplate: HTMLProductionTemplate( 378 | suppressUncessaryPrettyPrintAtAnchors: true 379 | ) 380 | ), 381 | """ 382 | 383 |
Picture missing. No alternative text available.
384 | """ 385 | ) 386 | } 387 | 388 | func testGettingPublicIDAndRoot() throws { 389 | 390 | let source = """ 391 | 392 | 393 | 394 | My Book Title 395 | 396 | """ 397 | 398 | let documentProperties = try XDocumentSource.text(source).readDocumentProperties() 399 | 400 | XCTAssertEqual(documentProperties.xmlVersion, "1.0") 401 | XCTAssertEqual(documentProperties.encoding, "us-ascii") 402 | XCTAssertEqual(documentProperties.standalone, "no") 403 | XCTAssertEqual(documentProperties.name, "theName") 404 | XCTAssertEqual(documentProperties.publicID, "the public identifier") 405 | XCTAssertEqual(documentProperties.systemID, "the system identifier") 406 | XCTAssertEqual(documentProperties.root?.serialized, """ 407 | 408 | """) 409 | } 410 | 411 | } 412 | 413 | /// An error with a description. 414 | /// 415 | /// When printing such an error, its descrition is printed. 416 | public struct ErrorWithDescription: LocalizedError, CustomStringConvertible { 417 | 418 | private let message: String 419 | 420 | public init(_ message: String?) { 421 | self.message = message ?? "(unkown error))" 422 | } 423 | 424 | public var description: String { message } 425 | 426 | public var errorDescription: String? { message } 427 | } 428 | 429 | extension String { 430 | 431 | /// Trimming all whitespace. 432 | func trimming() -> String { 433 | return self.self.trimmingLeft().trimmingRight() 434 | } 435 | 436 | /// Trimming left whitespace. 437 | func trimmingLeft() -> String { 438 | guard let index = firstIndex(where: { !CharacterSet(charactersIn: String($0)).isSubset(of: .whitespacesAndNewlines) }) else { 439 | return "" 440 | } 441 | return String(self[index...]) 442 | } 443 | 444 | /// Trimming right whitespace. 445 | func trimmingRight() -> String { 446 | guard let index = lastIndex(where: { !CharacterSet(charactersIn: String($0)).isSubset(of: .whitespacesAndNewlines) }) else { 447 | return "" 448 | } 449 | return String(self[...index]) 450 | } 451 | 452 | } 453 | -------------------------------------------------------------------------------- /Sources/SwiftXML/Builder/XParseBuilder.swift: -------------------------------------------------------------------------------- 1 | //===--- XParseBuilder.swift ----------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | #if canImport(FoundationEssentials) 12 | import FoundationEssentials 13 | #else 14 | import Foundation 15 | #endif 16 | 17 | import SwiftXMLInterfaces 18 | 19 | fileprivate func makePrefix(forName name: String, andURI uri: String) -> String { 20 | if !name.contains(":"), !name.isEmpty { 21 | name 22 | } else if let lastURIComponent = uri.split(separator: "/", omittingEmptySubsequences: true).last?.lowercased(), 23 | let prefix = lastURIComponent.split(separator: ":").last, !prefix.isEmpty { 24 | String(prefix) 25 | } else if let prefix = name.split(separator: ":").last, !prefix.isEmpty { 26 | String(prefix) 27 | } else { 28 | "a" 29 | } 30 | } 31 | 32 | public final class XParseBuilder: XEventHandler { 33 | 34 | public func parsingTime(seconds: Double) { 35 | // - 36 | } 37 | 38 | let document: XDocument 39 | let namespaceAware: Bool 40 | let silentEmptyRootPrefix: Bool 41 | let keepComments: Bool 42 | let keepCDATASections: Bool 43 | let externalWrapperElement: String? 44 | 45 | var currentBranch: XBranchInternal 46 | 47 | var prefixes = Set() 48 | var prefixCorrections = [String:String]() 49 | var resultingNamespaceURIToPrefix = [String:String]() 50 | var resultingPrefixToNamespaceURI = [String:String]() 51 | var namespaceURIAndPrefixDuringBuild = [(String,String)]() 52 | var prefixFreeNSURIsCount = 0 53 | 54 | let registeringAttributes: AttributeRegisterMode 55 | let registeringAttributeValuesFor: AttributeRegisterMode 56 | let registeringAttributesForNamespaces: AttributeWithNamespaceURIRegisterMode 57 | let registeringAttributeValuesForForNamespaces: AttributeWithNamespaceURIRegisterMode 58 | 59 | public init( 60 | document: XDocument, 61 | namespaceAware: Bool = false, 62 | silentEmptyRootPrefix: Bool = false, 63 | keepComments: Bool = false, 64 | keepCDATASections: Bool = false, 65 | externalWrapperElement: String? = nil, 66 | registeringAttributes: AttributeRegisterMode = .none, 67 | registeringAttributeValuesFor: AttributeRegisterMode = .none, 68 | registeringAttributesForNamespaces: AttributeWithNamespaceURIRegisterMode = .none, 69 | registeringAttributeValuesForForNamespaces: AttributeWithNamespaceURIRegisterMode = .none 70 | ) { 71 | 72 | self.document = document 73 | self.namespaceAware = namespaceAware 74 | self.silentEmptyRootPrefix = silentEmptyRootPrefix 75 | self.keepComments = keepComments 76 | self.keepCDATASections = keepCDATASections 77 | self.externalWrapperElement = externalWrapperElement 78 | 79 | self.registeringAttributes = registeringAttributes 80 | self.registeringAttributeValuesFor = registeringAttributeValuesFor 81 | self.registeringAttributesForNamespaces = registeringAttributesForNamespaces 82 | self.registeringAttributeValuesForForNamespaces = registeringAttributeValuesForForNamespaces 83 | 84 | self.currentBranch = document 85 | 86 | document._attributeRegisterMode = registeringAttributes 87 | document._attributeValueRegisterMode = registeringAttributeValuesFor 88 | } 89 | 90 | public func documentStart() -> Bool { true } 91 | 92 | public func enterExternalDataSource(data: Data, entityName: String?, systemID: String, url: URL?, textRange _: XTextRange?, dataRange _: XDataRange?) -> Bool { 93 | if let elementName = externalWrapperElement { 94 | var attributes = [String:String]() 95 | attributes["name"] = entityName 96 | attributes["systemID"] = systemID 97 | attributes["path"] = url?.path 98 | _ = elementStart( 99 | name: elementName, 100 | attributes: &attributes, 101 | textRange: nil, 102 | dataRange: nil 103 | ) 104 | } 105 | return true 106 | } 107 | 108 | public func leaveExternalDataSource() -> Bool { 109 | if let elementName = externalWrapperElement { 110 | _ = elementEnd(name: elementName, textRange: nil, dataRange: nil) 111 | } 112 | return true 113 | } 114 | 115 | public func enterInternalDataSource(data: Data, entityName: String, textRange: XTextRange?, dataRange: XDataRange?) -> Bool { 116 | return true 117 | } 118 | 119 | public func leaveInternalDataSource() -> Bool { 120 | return true 121 | } 122 | 123 | public func xmlDeclaration(version: String, encoding: String?, standalone: String?, textRange _: XTextRange?, dataRange _: XDataRange?) -> Bool { 124 | document.xmlVersion = version 125 | if let theEncoding = encoding { 126 | document.encoding = theEncoding 127 | } 128 | if let theStandalone = standalone { 129 | document.standalone = theStandalone 130 | } 131 | return true 132 | } 133 | 134 | public func documentTypeDeclarationStart(name: String, publicID: String?, systemID: String?, textRange _: XTextRange?, dataRange _: XDataRange?) -> Bool { 135 | document.name = name 136 | document.publicID = publicID 137 | document.systemID = systemID 138 | return true 139 | } 140 | 141 | public func documentTypeDeclarationEnd(textRange _: XTextRange?, dataRange _: XDataRange?) -> Bool { 142 | return true 143 | } 144 | 145 | public func elementStart(name: String, attributes: inout [String:String], textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 146 | 147 | let element = XElement(name) 148 | 149 | if namespaceAware { 150 | var namespaceDefinitionCount = 0 151 | 152 | for attributeName in attributes.keys { 153 | 154 | var uri: String? = nil 155 | var originalPrefix: String? = nil 156 | var proposedPrefix: String? = nil 157 | var existingPrefix: String? = nil 158 | 159 | if attributeName.hasPrefix("xmlns:") { 160 | uri = attributes[attributeName] 161 | existingPrefix = resultingNamespaceURIToPrefix[uri!] 162 | originalPrefix = String(attributeName.dropFirst(6)) 163 | proposedPrefix = existingPrefix ?? originalPrefix 164 | } else if attributeName == "xmlns" { 165 | uri = attributes[attributeName] 166 | if silentEmptyRootPrefix && currentBranch is XDocument { 167 | resultingNamespaceURIToPrefix[uri!] = "" 168 | attributes[attributeName] = nil 169 | } else { 170 | existingPrefix = resultingNamespaceURIToPrefix[uri!] 171 | originalPrefix = "" 172 | proposedPrefix = existingPrefix ?? makePrefix(forName: name, andURI: uri!) 173 | element.attached["prefixFreeNS"] = true 174 | prefixFreeNSURIsCount += 1 175 | } 176 | } 177 | 178 | if let uri, let originalPrefix, let proposedPrefix { 179 | namespaceDefinitionCount += 1 180 | if existingPrefix == nil { 181 | var resultingPrefix = proposedPrefix 182 | var avoidPrefixClashCount = 1 183 | while prefixes.contains(resultingPrefix) { 184 | avoidPrefixClashCount += 1 185 | resultingPrefix = "\(proposedPrefix)\(avoidPrefixClashCount)" 186 | } 187 | resultingNamespaceURIToPrefix[uri] = resultingPrefix 188 | resultingPrefixToNamespaceURI[resultingPrefix] = uri 189 | prefixes.insert(resultingPrefix) 190 | } 191 | namespaceURIAndPrefixDuringBuild.append((uri,originalPrefix)) 192 | attributes[attributeName] = nil 193 | } 194 | } 195 | if namespaceDefinitionCount > 0 { 196 | element.attached["nsCount"] = namespaceDefinitionCount 197 | } 198 | 199 | var prefixOfElement: String? = nil 200 | let colon = name.firstIndex(of: ":") 201 | if let colon { 202 | prefixOfElement = String(name[.. 0 { 204 | prefixOfElement = "" 205 | } 206 | 207 | if let prefixOfElement { 208 | var i = namespaceURIAndPrefixDuringBuild.count - 1 209 | while i >= 0 { 210 | let (uri,prefix) = namespaceURIAndPrefixDuringBuild[i] 211 | if prefix == prefixOfElement { 212 | element.prefix = resultingNamespaceURIToPrefix[uri]! 213 | if let colon { 214 | element.name = String(name[colon...].dropFirst()) 215 | } 216 | break 217 | } 218 | i -= 1 219 | } 220 | if i < 0 { // no namespace found: 221 | // this prefix cannot be used for namespaces ("dead prefix")! 222 | if prefixes.contains(prefixOfElement) { 223 | // too late, we have to correct later: 224 | if prefixCorrections[prefixOfElement] == nil { 225 | var avoidPrefixClashCount = 2 226 | var corrected = "\(prefixOfElement)\(avoidPrefixClashCount)" 227 | while prefixes.contains(corrected) { 228 | avoidPrefixClashCount += 1 229 | corrected = "\(prefixOfElement)\(avoidPrefixClashCount)" 230 | } 231 | prefixCorrections[prefixOfElement] = corrected 232 | prefixes.insert(corrected) 233 | } 234 | } else { 235 | // we just avoid this prefix: 236 | prefixes.insert(prefixOfElement) 237 | } 238 | } 239 | } 240 | } 241 | 242 | currentBranch._add(element) 243 | 244 | if namespaceAware { 245 | for (attributeName,attributeValue) in attributes { 246 | var prefixOfAttribute: String? = nil 247 | var attributeNameIfPrefix: String? = nil 248 | 249 | var literalPrefixOfAttribute: String? = nil 250 | let colon = attributeName.firstIndex(of: ":") 251 | if let colon { 252 | literalPrefixOfAttribute = String(attributeName[..= 0 { 257 | let (uri,prefix) = namespaceURIAndPrefixDuringBuild[i] 258 | if prefix == literalPrefixOfAttribute { 259 | prefixOfAttribute = resultingNamespaceURIToPrefix[uri]! 260 | attributeNameIfPrefix = String(attributeName[colon!...].dropFirst()) 261 | break 262 | } 263 | i -= 1 264 | } 265 | if i < 0 { // no namespace found: 266 | // this prefix cannot be used for namespaces ("dead prefix")! 267 | if prefixes.contains(literalPrefixOfAttribute) { 268 | // too late, we have to correct later: 269 | if prefixCorrections[literalPrefixOfAttribute] == nil { 270 | var avoidPrefixClashCount = 2 271 | var corrected = "\(literalPrefixOfAttribute)\(avoidPrefixClashCount)" 272 | while prefixes.contains(corrected) { 273 | avoidPrefixClashCount += 1 274 | corrected = "\(literalPrefixOfAttribute)\(avoidPrefixClashCount)" 275 | } 276 | prefixCorrections[literalPrefixOfAttribute] = corrected 277 | prefixes.insert(corrected) 278 | } 279 | } else { 280 | // we just avoid this prefix: 281 | prefixes.insert(literalPrefixOfAttribute) 282 | } 283 | } 284 | } 285 | 286 | if let prefixOfAttribute { 287 | let attributeNameIfPrefix = attributeNameIfPrefix! 288 | element[prefixOfAttribute,attributeNameIfPrefix] = attributeValue 289 | 290 | do { 291 | let registerUsingNamespaceURIsAndName = switch registeringAttributesForNamespaces { 292 | case .none: 293 | false 294 | case .selected(let selection): 295 | selection.contains(where: { $0.namespaceURI == resultingPrefixToNamespaceURI[prefixOfAttribute] && $0.name == attributeNameIfPrefix}) 296 | case .all: 297 | true 298 | } 299 | if registerUsingNamespaceURIsAndName { 300 | let attributeProperties = AttributeProperties(value: attributeValue, element: element) 301 | element._registeredAttributesWithPrefix[prefixOfAttribute,attributeNameIfPrefix] = attributeProperties 302 | document.registerAttributeWithPrefix(attributeProperties: attributeProperties, withPrefix: prefixOfAttribute, withName: attributeNameIfPrefix) 303 | } 304 | } 305 | 306 | do { 307 | let registerForValue = switch registeringAttributeValuesForForNamespaces { 308 | case .none: 309 | false 310 | case .selected(let selection): 311 | selection.contains(where: { $0.namespaceURI == resultingPrefixToNamespaceURI[prefixOfAttribute] && $0.name == attributeNameIfPrefix}) 312 | case .all: 313 | true 314 | } 315 | if registerForValue { 316 | let attributeProperties = AttributeProperties(value: attributeValue, element: element) 317 | element._registeredAttributeWithPrefixValues[prefixOfAttribute,attributeNameIfPrefix] = attributeProperties 318 | document.registerAttributeWithPrefixValue(attributeProperties: attributeProperties, withPrefix: prefixOfAttribute, withName: attributeNameIfPrefix) 319 | } 320 | } 321 | 322 | } else { 323 | element[attributeName] = attributeValue 324 | } 325 | } 326 | } else { 327 | element.setAttributes(attributes: attributes) 328 | } 329 | 330 | currentBranch = element 331 | element._sourceRange = textRange 332 | 333 | return true 334 | } 335 | 336 | public func elementEnd(name: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 337 | 338 | if namespaceAware, let element = currentBranch as? XElement, let namespaceDefinitionCount = element.attached["nsCount"] as? Int { 339 | namespaceURIAndPrefixDuringBuild.removeLast(namespaceDefinitionCount) 340 | element.attached["nsCount"] = nil 341 | if element.attached["prefixFreeNS"] as? Bool == true { 342 | prefixFreeNSURIsCount -= 1 343 | element.attached["prefixFreeNS"] = nil 344 | } 345 | } 346 | 347 | if let endTagTextRange = textRange, let element = currentBranch as? XElement, let startTagTextRange = element._sourceRange { 348 | element._sourceRange = XTextRange( 349 | startLine: startTagTextRange.startLine, 350 | startColumn: startTagTextRange.startColumn, 351 | endLine: endTagTextRange.endLine, 352 | endColumn: endTagTextRange.endColumn 353 | ) 354 | } 355 | if let parent = currentBranch._parent { 356 | currentBranch = parent 357 | } 358 | else { 359 | currentBranch = document 360 | } 361 | 362 | return true 363 | } 364 | 365 | public func text(text: String, whitespace: WhitespaceIndicator, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 366 | let node = XText(text, whitespace: whitespace) 367 | node._sourceRange = textRange 368 | currentBranch._add(node) 369 | return true 370 | } 371 | 372 | public func cdataSection(text: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 373 | let node = keepCDATASections ? XCDATASection(text): XText(text) 374 | node._sourceRange = textRange 375 | currentBranch._add(node) 376 | return true 377 | } 378 | 379 | public func internalEntity(name: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 380 | let node = XInternalEntity(name) 381 | node._sourceRange = textRange 382 | currentBranch._add(node) 383 | return true 384 | } 385 | 386 | public func externalEntity(name: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 387 | let node = XExternalEntity(name) 388 | node._sourceRange = textRange 389 | currentBranch._add(node) 390 | return true 391 | } 392 | 393 | public func processingInstruction(target: String, data: String?, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 394 | let node = XProcessingInstruction(target: target, data: data) 395 | node._sourceRange = textRange 396 | currentBranch._add(node) 397 | return true 398 | } 399 | 400 | public func comment(text: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 401 | if keepComments { 402 | let node = XComment(text, withAdditionalSpace: false) 403 | node._sourceRange = textRange 404 | currentBranch._add(node) 405 | } 406 | return true 407 | } 408 | 409 | public func internalEntityDeclaration(name: String, value: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 410 | let decl = XInternalEntityDeclaration(name: name, value: value) 411 | decl._sourceRange = textRange 412 | document.internalEntityDeclarations[name] = decl 413 | return true 414 | } 415 | 416 | public func externalEntityDeclaration(name: String, publicID: String?, systemID: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 417 | let decl = XExternalEntityDeclaration(name: name, publicID: publicID, systemID: systemID) 418 | decl._sourceRange = textRange 419 | document.externalEntityDeclarations[name] = decl 420 | return true 421 | } 422 | 423 | public func unparsedEntityDeclaration(name: String, publicID: String?, systemID: String, notation: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 424 | let decl = XUnparsedEntityDeclaration(name: name, publicID: publicID, systemID: systemID, notationName: notation) 425 | decl._sourceRange = textRange 426 | document.unparsedEntityDeclarations[name] = decl 427 | return true 428 | } 429 | 430 | public func notationDeclaration(name: String, publicID: String?, systemID: String?, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 431 | let decl = XNotationDeclaration(name: name, publicID: publicID, systemID: systemID) 432 | decl._sourceRange = textRange 433 | document.notationDeclarations[name] = decl 434 | return true 435 | } 436 | 437 | public func elementDeclaration(name: String, literal: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 438 | let decl = XElementDeclaration(name: name, literal: literal) 439 | decl._sourceRange = textRange 440 | document.elementDeclarations[name] = decl 441 | return true 442 | } 443 | 444 | public func attributeListDeclaration(name: String, literal: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 445 | let decl = XAttributeListDeclaration(name: name, literal: literal) 446 | decl._sourceRange = textRange 447 | document.attributeListDeclarations[name] = decl 448 | return true 449 | } 450 | 451 | public func parameterEntityDeclaration(name: String, value: String, textRange: XTextRange?, dataRange _: XDataRange?) -> Bool { 452 | let decl = XParameterEntityDeclaration(name: name, value: value) 453 | decl._sourceRange = textRange 454 | document.parameterEntityDeclarations[name] = decl 455 | return true 456 | } 457 | 458 | public func documentEnd() -> Bool { 459 | 460 | document._attributeWithPrefixRegisterMode = 461 | switch registeringAttributesForNamespaces { 462 | case .none: 463 | .none 464 | case .selected(let selection): 465 | .selected(selection.map{ PrefixedName.make(fromPrefix: resultingNamespaceURIToPrefix[$0.namespaceURI], andName: $0.name) }.compactMap{ $0 }) 466 | case .all: 467 | .all 468 | } 469 | 470 | document._attributeWithPrefixValueRegisterMode = 471 | switch registeringAttributeValuesForForNamespaces { 472 | case .none: 473 | .none 474 | case .selected(let selection): 475 | .selected(selection.map{ PrefixedName.make(fromPrefix: resultingNamespaceURIToPrefix[$0.namespaceURI], andName: $0.name) }.compactMap{ $0 }) 476 | case .all: 477 | .all 478 | } 479 | 480 | if namespaceAware { 481 | if prefixCorrections.isEmpty { 482 | for (uri,prefix) in resultingNamespaceURIToPrefix { 483 | document._namespaceURIToPrefix[uri] = prefix 484 | document._prefixToNamespaceURI[prefix] = uri 485 | document._prefixes.insert(prefix) 486 | } 487 | } else { 488 | for (uri,prefix) in resultingNamespaceURIToPrefix { 489 | let correctedPrefix = prefixCorrections[prefix] ?? prefix 490 | document._namespaceURIToPrefix[uri] = correctedPrefix 491 | document._prefixToNamespaceURI[correctedPrefix] = uri 492 | document._prefixes.insert(correctedPrefix) 493 | } 494 | for element in document.descendants { 495 | if let prefix = element.prefix, let correctedPrefix = prefixCorrections[prefix] { 496 | element.prefix = correctedPrefix 497 | if let attributesForThisPrefix = element._attributesForPrefix.secondKeys(forLeftKey: prefix) { 498 | for attributeName in attributesForThisPrefix { 499 | element[correctedPrefix,attributeName] = element[prefix,attributeName] 500 | element[prefix,attributeName] = nil 501 | } 502 | } 503 | } 504 | } 505 | } 506 | } 507 | 508 | return true 509 | } 510 | 511 | } 512 | -------------------------------------------------------------------------------- /Tests/SwiftXMLTests/FromReadme.swift: -------------------------------------------------------------------------------- 1 | //===--- FromReadme.swift ----------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | import XCTest 12 | import class Foundation.Bundle 13 | @testable import SwiftXML 14 | 15 | final class FromReadmeTests: XCTestCase { 16 | 17 | func testFirstExample() throws { 18 | 19 | let textAllowedInElementWithName = ["title", "td"] 20 | 21 | let document = try parseXML( 22 | fromText: """ 23 | 24 | 25 | A table with numbers 26 | 27 | 28 | 29 | 30 |
78
31 |
32 | """, 33 | registeringAttributes: .selected(["label"]), 34 | textAllowedInElementWithName: textAllowedInElementWithName 35 | ) 36 | 37 | let transformation = XTransformation { 38 | 39 | XRule(forRegisteredAttributes: "label") { label in 40 | label.element["label"] = "(\(label.value))" 41 | } 42 | 43 | XRule(forElements: "caption") { caption in 44 | caption.name = "paragraph" 45 | caption["role"] = "caption" 46 | } 47 | 48 | XRule(forElements: "table") { table in 49 | table.insertNext { 50 | XElement("caption") { 51 | if let label = table["label"] { 52 | "Table "; label; ": " 53 | } 54 | table.firstChild("title")?.replacedBy { $0.content } 55 | } 56 | } 57 | table["label"] = nil 58 | } 59 | 60 | } 61 | 62 | transformation.execute(inDocument: document) 63 | 64 | XCTAssertEqual( 65 | document.serialized(pretty: true, textAllowedInElementWithName: textAllowedInElementWithName), 66 | """ 67 | 68 | 69 | 70 | 71 | 72 | 73 |
78
74 | Table (1): A table with numbers 75 |
76 | """ 77 | ) 78 | } 79 | 80 | func testFirstExampleWithReplacedByForSequence() throws { 81 | 82 | let document = try parseXML( 83 | fromText: """ 84 | 85 | 86 | A table 87 | with numbers 88 | 89 | 90 | 91 | 92 |
78
93 |
94 | """, 95 | registeringAttributes: .selected(["label"]), 96 | textAllowedInElementWithName: ["title", "td"] 97 | ) 98 | 99 | let transformation = XTransformation { 100 | 101 | XRule(forRegisteredAttributes: "label") { label in 102 | label.element["label"] = "(\(label.value))" 103 | } 104 | 105 | XRule(forElements: "caption") { caption in 106 | caption.name = "paragraph" 107 | caption["role"] = "caption" 108 | } 109 | 110 | XRule(forElements: "table") { table in 111 | table.insertNext { 112 | XElement("caption") { 113 | if let label = table["label"] { 114 | "Table "; label; ": " 115 | } 116 | table.children("title").replacedBy { $0.content } 117 | } 118 | } 119 | table["label"] = nil 120 | } 121 | 122 | } 123 | 124 | transformation.execute(inDocument: document) 125 | 126 | XCTAssertEqual(document.serialized(pretty: true), """ 127 | 128 | 129 | 130 | 131 | 132 | 133 |
78
134 | Table (1): A table with numbers 135 |
136 | """) 137 | } 138 | 139 | func testParsingAndJoiningIDs() throws { 140 | let document = try parseXML(fromText: """ 141 | 142 | 143 | 144 | 145 | 146 | """) 147 | 148 | XCTAssertEqual(document.children.children["id"].joined(separator: ", "), "1, 2, 3") 149 | } 150 | 151 | func testRemoveElementsWhileIteration() throws{ 152 | let document = try parseXML(fromText: """ 153 | 154 | """) 155 | 156 | document.traverse { content in 157 | if let element = content as? XElement, element["remove"] == "true" { 158 | element.remove() 159 | } 160 | } 161 | 162 | XCTAssertEqual(document.children.children["id"].joined(separator: ", "), "2, 4") 163 | } 164 | 165 | func testPrintContentWithSourceRanges() throws{ 166 | let document = try parseXML(fromText: """ 167 | 168 | Hello 169 | 170 | """, textAllowedInElementWithName: ["b"]) 171 | 172 | XCTAssertEqual( 173 | document.allContent.map{ "\($0.sourceRange!): \($0)" }.joined(separator: "\n"), 174 | """ 175 | 1:1 - 3:4: 176 | 2:5 - 2:16: 177 | 2:8 - 2:12: "Hello" 178 | """ 179 | ) 180 | } 181 | 182 | func testExistingItems() throws{ 183 | let document = try parseXML(fromText: """ 184 | 185 | """) 186 | 187 | if let theBs = document.descendants("b").existing { 188 | XCTAssertEqual(theBs["id"].joined(separator: ", "), "1, 2, 3") 189 | } 190 | } 191 | 192 | func testContentSequenceCondition() throws{ 193 | let document = try parseXML(fromText: """ 194 | 195 | """) 196 | 197 | for descendant in document.descendants({ element in element["take"] == "true" }) { 198 | XCTAssertEqual(descendant["take"]!, "true") 199 | } 200 | } 201 | 202 | func testChainedIterators() throws{ 203 | let document = try parseXML(fromText: """ 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | """) 212 | 213 | var output = "" 214 | for element in document.descendants.descendants { 215 | output += element.serialized(pretty: true) 216 | } 217 | output = output.replacing(" ", with: "").replacing("\n", with: "") 218 | XCTAssertEqual(output, "") 219 | } 220 | 221 | func testFirstChildOfEachChild() throws{ 222 | let element = XElement("z") { 223 | XElement("a") { 224 | XElement("a1") 225 | XElement("a2") 226 | } 227 | XElement("b") { 228 | XElement("b1") 229 | XElement("b2") 230 | } 231 | } 232 | 233 | var output = "" 234 | for element in element.children.map({ $0.children.first }) { output += element?.name ?? "-" } 235 | XCTAssertEqual(output, "a1b1") 236 | } 237 | 238 | func testReplaceElementInHierarchy() throws{ 239 | let b = XElement("b") 240 | 241 | let a = XElement("a") { 242 | b 243 | "Hello" 244 | } 245 | 246 | let expectedOutputBeforeReplace = "Hello" 247 | 248 | XCTAssertEqual(a.serialized(), expectedOutputBeforeReplace) 249 | 250 | b.replace { 251 | XElement("wrapper1") { 252 | b 253 | XElement("wrapper2") { 254 | b.next 255 | } 256 | } 257 | } 258 | 259 | let expectedOutputAfterReplace = """ 260 | 261 | 262 | 263 | Hello 264 | 265 | 266 | """ 267 | 268 | XCTAssertEqual(a.serialized(pretty: true), expectedOutputAfterReplace) 269 | } 270 | 271 | func testConsumeForeignTypeAsXML() throws { 272 | 273 | struct MyStruct: XContentConvertible { 274 | 275 | let text1: String 276 | let text2: String 277 | 278 | func collectXML(by xmlCollector: inout XMLCollector) { 279 | xmlCollector.collect(XElement("text1") { text1 }) 280 | xmlCollector.collect(XElement("text2") { text2 }) 281 | } 282 | 283 | } 284 | 285 | let myStruct1 = MyStruct(text1: "hello", text2: "world") 286 | let myStruct2 = MyStruct(text1: "greeting", text2: "you") 287 | 288 | let element = XElement("x") { 289 | myStruct1 290 | myStruct2 291 | } 292 | 293 | XCTAssertEqual(element.serialized(pretty: true), #""" 294 | 295 | hello 296 | world 297 | greeting 298 | you 299 | 300 | """# 301 | ) 302 | } 303 | 304 | func testAddElementToDocument() throws { 305 | let document = try parseXML(fromText: """ 306 | 307 | """) 308 | 309 | for element in document.elements("b") { 310 | if element["id"] == "2" { 311 | element.insertNext { 312 | XElement("c") { 313 | element.previous 314 | } 315 | } 316 | } 317 | } 318 | 319 | let expectedOutput = """ 320 | 321 | """ 322 | 323 | XCTAssertEqual(document.serialized(), expectedOutput) 324 | } 325 | 326 | func testElementContentManipulation() throws { 327 | let element = XElement("top") { 328 | XElement("a1") { 329 | XElement("a2") 330 | } 331 | XElement("b1") { 332 | XElement("b2") 333 | } 334 | XElement("c1") { 335 | XElement("c2") 336 | } 337 | } 338 | 339 | XCTAssertEqual(element.serialized(), "") 340 | 341 | // ---- 1 ---- 342 | 343 | for content in element.content { 344 | content.replace(.skipping) { 345 | content.content 346 | } 347 | } 348 | 349 | XCTAssertEqual(element.serialized(), "") 350 | 351 | // ---- 2 ---- 352 | 353 | for content in element.contentReversed { 354 | content.insertPrevious(.skipping) { 355 | XElement("I" + ((content as? XElement)?.name ?? "?")) 356 | } 357 | } 358 | 359 | XCTAssertEqual(element.serialized(), "") 360 | } 361 | 362 | func testAddElementToDescendants() throws { 363 | let e = XElement("a") { 364 | XElement("b") 365 | XElement("c") 366 | } 367 | 368 | for descendant in e.descendants({ $0.name != "added" }) { 369 | descendant.add { XElement("added") } 370 | } 371 | 372 | XCTAssertEqual(e.serialized(), "") 373 | } 374 | 375 | func testAddElementToSelectedDescendants() throws { 376 | let myElement = XElement("a") { 377 | XElement("to-add") 378 | XElement("b") 379 | XElement("c") 380 | } 381 | 382 | for descendant in myElement.descendants({ $0.name != "to-add" }) { 383 | descendant.add { 384 | myElement.descendants("to-add") 385 | } 386 | } 387 | 388 | XCTAssertEqual(myElement.serialized(), "") 389 | } 390 | 391 | func testInsertNextElementToSelectedDescendants() throws { 392 | let myElement = XElement("top") { 393 | XElement("a") 394 | } 395 | 396 | for element in myElement.descendants { 397 | if element.name == "a" { 398 | element.insertNext() { 399 | XElement("b") 400 | } 401 | } 402 | else if element.name == "b" { 403 | element.insertNext { 404 | XElement("c") 405 | } 406 | } 407 | } 408 | 409 | XCTAssertEqual(myElement.serialized(), "") 410 | } 411 | 412 | func testInsertNextElementToSelectedDescendantsButSkipping() throws { 413 | let myElement = XElement("top") { 414 | XElement("a") 415 | } 416 | 417 | for element in myElement.descendants { 418 | if element.name == "a" { 419 | element.insertNext(.skipping) { 420 | XElement("b") 421 | } 422 | } 423 | else if element.name == "b" { 424 | element.insertNext { 425 | XElement("c") 426 | } 427 | } 428 | } 429 | 430 | XCTAssertEqual(myElement.serialized(), "") 431 | } 432 | 433 | func testReplaceNodeWithContent() throws { 434 | let document = try parseXML(fromText: """ 435 | Hello 436 | """) 437 | for bold in document.descendants("bold") { bold.replace { bold.content } } 438 | 439 | XCTAssertEqual(document.serialized(), "Hello") 440 | } 441 | 442 | func testPulling() { 443 | 444 | do { 445 | let firstFence1 = XElement("fence") { "u" } 446 | let t: String = firstFence1.applying{ $0.remove() }.pulling{ ($0.content({ $0 is XText }).first as? XText)?.value ?? "" } 447 | XCTAssertEqual(t, "u") 448 | } 449 | 450 | do { 451 | let firstFence2 = XElement("fence") { "" } 452 | let t: String = firstFence2.applying{ $0.remove() }.pulling{ ($0.content({ $0 is XText }).first as? XText)?.value ?? "" } 453 | XCTAssertEqual(t, "") 454 | } 455 | 456 | } 457 | 458 | func testDescendants() throws { 459 | let element = XElement("z") { 460 | XElement("a") { 461 | XElement("a1") 462 | XElement("a2") 463 | } 464 | XElement("b") { 465 | XElement("b1") 466 | XElement("b2") 467 | } 468 | } 469 | 470 | XCTAssertEqual(element.descendants.map{ $0.description }.joined(separator: ", "), ", , , , , ") 471 | } 472 | 473 | func testWithAndWhen() throws { 474 | 475 | let element1 = XElement("a") { 476 | XElement("child-of-a") { 477 | XElement("more", ["special": "yes"]) 478 | } 479 | } 480 | 481 | let element2 = XElement("b") 482 | 483 | if let childOfA = element1.fullfilling({ $0.name == "a" })?.children.first, 484 | childOfA.children.first?.fullfills({ $0["special"] == "yes" && $0["moved"] != "yes" }) == true { 485 | element2.add { 486 | childOfA.applying { $0["moved"] = "yes" } 487 | } 488 | } 489 | 490 | XCTAssertEqual(element2.serialized(), #""#) 491 | } 492 | 493 | func testApplyingForSequence() throws { 494 | 495 | let myElement = XElement("a") { 496 | XElement("b", ["inserted": "yes"]) { 497 | XElement("c", ["inserted": "yes"]) 498 | } 499 | } 500 | 501 | let inserted = Array(myElement.descendants.filter{ $0["inserted"] == "yes" }.applying{ $0["found"] = "yes" }) 502 | 503 | XCTAssertEqual(inserted.description, #"[, ]"#) 504 | } 505 | 506 | 507 | func testTransformationWithInverseOrder() throws { 508 | 509 | let document = try parseXML(fromText: """ 510 | 511 |
512 | 513 | This is a hint. 514 | 515 | 516 | This is a warning. 517 | 518 |
519 |
520 | """, textAllowedInElementWithName: ["paragraph"]) 521 | 522 | let transformation = XTransformation { 523 | 524 | XRule(forElements: "paragraph") { element in 525 | let style: String? = if element.parent?.name == "warning" { 526 | "color:Red" 527 | } else { 528 | nil 529 | } 530 | element.replace { 531 | XElement("p", ["style": style]) { 532 | element.content 533 | } 534 | } 535 | } 536 | 537 | XRule(forElements: "hint", "warning") { element in 538 | element.replace { 539 | XElement("div") { 540 | XElement("p", ["style": "bold"]) { 541 | element.name.uppercased() 542 | } 543 | element.content 544 | } 545 | } 546 | } 547 | } 548 | 549 | transformation.execute(inDocument: document) 550 | 551 | XCTAssertEqual( 552 | document.serialized(pretty: true), 553 | """ 554 | 555 |
556 |
557 |

HINT

558 |

This is a hint.

559 |
560 |
561 |

WARNING

562 |

This is a warning.

563 |
564 |
565 |
566 | """ 567 | ) 568 | } 569 | 570 | func testTransformationWithAnnotations() throws { 571 | 572 | let document = try parseXML(fromText: """ 573 | 574 |
575 | 576 | This is a hint. 577 | 578 | 579 | This is a warning. 580 | 581 |
582 |
583 | """, textAllowedInElementWithName: ["paragraph"]) 584 | 585 | let transformation = XTransformation { 586 | 587 | XRule(forElements: "hint", "warning") { element in 588 | element.replace { 589 | XElement("div", attached: ["source": element.name]) { 590 | XElement("p", ["style": "bold"]) { 591 | element.name.uppercased() 592 | } 593 | element.content 594 | } 595 | } 596 | } 597 | 598 | XRule(forElements: "paragraph") { element in 599 | let style: String? = if element.parent?.attached["source"] as? String == "warning" { 600 | "color:Red" 601 | } else { 602 | nil 603 | } 604 | element.replace { 605 | XElement("p", ["style": style]) { 606 | element.content 607 | } 608 | } 609 | } 610 | } 611 | 612 | transformation.execute(inDocument: document) 613 | 614 | XCTAssertEqual( 615 | document.serialized(pretty: true), 616 | """ 617 | 618 |
619 |
620 |

HINT

621 |

This is a hint.

622 |
623 |
624 |

WARNING

625 |

This is a warning.

626 |
627 |
628 |
629 | """ 630 | ) 631 | } 632 | 633 | func testTransformationWithBackLinks() throws { 634 | 635 | let document = try parseXML(fromText: """ 636 | 637 |
638 | 639 | This is a hint. 640 | 641 | 642 | This is a warning. 643 | 644 |
645 |
646 | """, textAllowedInElementWithName: ["paragraph"]) 647 | 648 | let transformation = XTransformation { 649 | 650 | XRule(forElements: "hint", "warning") { element in 651 | element.replace { 652 | XElement("div", withBackLinkFrom: element) { 653 | XElement("p", ["style": "bold"]) { 654 | element.name.uppercased() 655 | } 656 | element.content 657 | } 658 | } 659 | } 660 | 661 | XRule(forElements: "paragraph") { element in 662 | let style: String? = if element.parent?.backlink?.name == "warning" { 663 | "color:Red" 664 | } else { 665 | nil 666 | } 667 | element.replace { 668 | XElement("p", ["style": style]) { 669 | element.content 670 | } 671 | } 672 | } 673 | } 674 | 675 | // make a clone with inverse backlinks, 676 | // pointing from the original document to the clone: 677 | document.makeVersion() 678 | do { 679 | let backlink: XDocument? = document.backlink 680 | XCTAssert(backlink != nil) 681 | } 682 | 683 | transformation.execute(inDocument: document) 684 | 685 | // remove the clone: 686 | document.forgetLastVersion() 687 | 688 | XCTAssertEqual( 689 | document.serialized(pretty: true), 690 | """ 691 | 692 |
693 |
694 |

HINT

695 |

This is a hint.

696 |
697 |
698 |

WARNING

699 |

This is a warning.

700 |
701 |
702 |
703 | """ 704 | ) 705 | } 706 | 707 | func testTransformWithTraversal() throws { 708 | 709 | let document = try parseXML(fromText: """ 710 | 711 |
712 | 713 | This is a hint. 714 | 715 | 716 | This is a warning. 717 | 718 |
719 |
720 | """, textAllowedInElementWithName: ["paragraph"]) 721 | 722 | for section in document.elements("section") { 723 | section.traverse { node in 724 | // - 725 | } up: { node in 726 | if let element = node as? XElement { 727 | guard node !== section else { return } 728 | switch element.name { 729 | case "paragraph": 730 | let style: String? = if element.parent?.name == "warning" { 731 | "color:Red" 732 | } else { 733 | nil 734 | } 735 | element.replace { 736 | XElement("p", ["style": style]) { 737 | element.content 738 | } 739 | } 740 | case "hint", "warning": 741 | element.replace { 742 | XElement("div") { 743 | XElement("p", ["style": "bold"]) { 744 | element.name.uppercased() 745 | } 746 | element.content 747 | } 748 | } 749 | default: 750 | break 751 | } 752 | } 753 | } 754 | } 755 | 756 | XCTAssertEqual( 757 | document.serialized(pretty: true), 758 | """ 759 | 760 |
761 |
762 |

HINT

763 |

This is a hint.

764 |
765 |
766 |

WARNING

767 |

This is a warning.

768 |
769 |
770 |
771 | """ 772 | ) 773 | } 774 | 775 | func testSubscriptsOfSequences() throws { 776 | 777 | let document = try parseXML( 778 | fromText: """ 779 | 780 | The Title 781 |

The first paragraph.

782 |

The second paragraph.

783 | This is the annex. 784 |
785 | """, 786 | textAllowedInElementWithName: ["title", "p", "annex"] 787 | ) 788 | 789 | XCTAssertEqual(document.children.children("p")["id"].joined(separator: " "), "1 2") 790 | XCTAssertEqual(document.children.children("p")[2]?.description ?? "-", #"

"#) 791 | XCTAssertEqual(document.children.children("p")[99]?.description ?? "-", "-") 792 | XCTAssertEqual(document.allTexts[2]?.value ?? "-", "The first paragraph.") 793 | } 794 | 795 | func testTextHandling() throws { 796 | 797 | let document = try parseXML(fromText: """ 798 | 799 | Hello world! 800 | world world world 801 | 802 | """) 803 | 804 | let searchText = "world" 805 | 806 | document.traverse { node in 807 | if let text = node as? XText { 808 | if text.value.contains(searchText) { 809 | text.isolated = true 810 | var addSearchText = false 811 | for part in text.value.components(separatedBy: searchText) { 812 | text.insertPrevious { 813 | if addSearchText { 814 | XElement("span", ["style": "background:LightGreen"]) { 815 | searchText 816 | } 817 | } 818 | part 819 | } 820 | addSearchText = true 821 | } 822 | text.remove() 823 | text.isolated = false 824 | } 825 | } 826 | } 827 | 828 | XCTAssertEqual(document.serialized(), """ 829 | 830 | Hello world! 831 | world world world 832 | 833 | """) 834 | } 835 | 836 | func testRegisteredAttributeValues() throws { 837 | let source = """ 838 | 839 | 840 | 841 | First reference to "1". 842 | Second reference to "1". 843 | 844 | """ 845 | 846 | let document = try parseXML(fromText: source, registeringAttributeValuesFor: .selected([ "id", "refid"])) 847 | 848 | XCTAssertEqual( 849 | """ 850 | id="1": 851 | \(document.registeredValues("1", forAttribute: "id").map{ $0.element.description }.joined(separator: "\n")) 852 | 853 | refid="1": 854 | \(document.registeredValues("1", forAttribute: "refid").map{ $0.element.serialized() }.joined(separator: "\n")) 855 | """, 856 | """ 857 | id="1": 858 | 859 | 860 | refid="1": 861 | First reference to "1". 862 | Second reference to "1". 863 | """ 864 | ) 865 | } 866 | 867 | func testProcessingInstructions() throws { 868 | 869 | let processingInstruction = XProcessingInstruction(target: "test", data: "hello") 870 | XCTAssertEqual(processingInstruction.target, "test") 871 | XCTAssertEqual(processingInstruction.data, "hello") 872 | XCTAssertEqual(processingInstruction.serialized, "") 873 | 874 | // except the first space character, whitespace and quotes are included in the data: 875 | XCTAssertEqual((try parseXML(fromText: "").firstContent as? XProcessingInstruction)?.data, " data ") 876 | XCTAssertEqual((try parseXML(fromText: #""#).firstContent as? XProcessingInstruction)?.data, #""data""#) 877 | XCTAssertEqual((try parseXML(fromText: "").firstContent as? XProcessingInstruction)?.data, "'data' ") 878 | 879 | // check target with "prefix": 880 | XCTAssertEqual((try parseXML(fromText: "").firstContent as? XProcessingInstruction)?.target, "prefix-type") 881 | } 882 | 883 | } 884 | -------------------------------------------------------------------------------- /Sources/SwiftXML/XML/Production.swift: -------------------------------------------------------------------------------- 1 | //===--- Production.swift -------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftXML.org open source project 4 | // 5 | // Copyright (c) 2021-2023 Stefan Springer (https://stefanspringer.com) 6 | // and the SwiftXML project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | //===----------------------------------------------------------------------===// 10 | 11 | // !!! currently FoundationEssentials does not have Filehandle !!! 12 | //#if canImport(FoundationEssentials) 13 | //import FoundationEssentials 14 | //#else 15 | import Foundation 16 | //#endif 17 | 18 | public let X_DEFAULT_INDENTATION = " " 19 | public let X_DEFAULT_LINEBREAK = "\n" 20 | 21 | /** 22 | The formatter has two changes over the XFormatter: 23 | - It writes to the file directly, no need to build complicated strings. 24 | - It uses the nodes of the XML tree directly. 25 | */ 26 | 27 | public protocol Writer { 28 | func write(_ text: String) throws 29 | } 30 | 31 | /// Do not forget to finally call `flush()`. 32 | public class BufferedFileWriter: Writer { 33 | 34 | private var file: FileHandle = FileHandle.standardOutput 35 | private var buffer: Data 36 | private let bufferSize: Int 37 | 38 | public static func using(_ file: FileHandle, bufferSize: Int? = nil, f: (Writer) throws -> ()) throws { 39 | let writer = BufferedFileWriter(file, bufferSize: bufferSize) 40 | try f(writer) 41 | try writer.flush() 42 | } 43 | 44 | public init(_ file: FileHandle, bufferSize: Int? = nil) { 45 | self.file = file 46 | self.bufferSize = bufferSize ?? 1024 * 1024 47 | self.buffer = Data(capacity: self.bufferSize) 48 | } 49 | 50 | open func write(_ text: String) throws { 51 | guard let data = text.data(using: .utf8) else { throw SwiftXMLError("could not convert text to data") } 52 | buffer.append(data) 53 | if buffer.count > bufferSize { 54 | try flush() 55 | } 56 | } 57 | 58 | public func flush() throws { 59 | try file.write(contentsOf: buffer) 60 | buffer.removeAll(keepingCapacity: true) 61 | } 62 | 63 | } 64 | 65 | public class CollectingWriter: Writer, CustomStringConvertible { 66 | 67 | public init() {} 68 | 69 | private var texts = [String]() 70 | 71 | public var description: String { get { texts.joined() } } 72 | 73 | public func write(_ text: String) { 74 | texts.append(text) 75 | } 76 | 77 | public func close() throws {} 78 | 79 | } 80 | 81 | public protocol XProductionTemplate { 82 | func activeProduction( 83 | for writer: Writer, 84 | withStartElement startElement: XElement?, 85 | prefixTranslations: [String:String]?, 86 | declarationSupressingNamespaceURIs: [String]? 87 | ) -> XActiveProduction 88 | } 89 | 90 | public protocol XActiveProduction { 91 | 92 | func write(_ text: String) throws 93 | 94 | func sortDeclarationsInInternalSubset(document: XDocument) -> [XDeclarationInInternalSubset] 95 | 96 | func writeDocumentStart(document: XDocument) throws 97 | 98 | func writeXMLDeclaration(version: String, encoding: String?, standalone: String?) throws 99 | 100 | func writeDocumentTypeDeclarationBeforeInternalSubset(type: String, publicID: String?, systemID: String?, hasInternalSubset: Bool) throws 101 | 102 | func writeDocumentTypeDeclarationInternalSubsetStart() throws 103 | 104 | func writeDocumentTypeDeclarationInternalSubsetEnd() throws 105 | 106 | func writeDocumentTypeDeclarationAfterInternalSubset(hasInternalSubset: Bool) throws 107 | 108 | func writeElementStartBeforeAttributes(element: XElement) throws 109 | 110 | func sortAttributeNames(attributeNames: [String], element: XElement) -> [String] 111 | 112 | func writeAttributeValue(name: String, value: String, element: XElement) throws 113 | 114 | func writeAttribute(name: String, value: String, element: XElement) throws 115 | 116 | func writeElementStartAfterAttributes(element: XElement) throws 117 | 118 | func writeElementEnd(element: XElement) throws 119 | 120 | func writeText(text: XText) throws 121 | 122 | func writeLiteral(literal: XLiteral) throws 123 | 124 | func writeCDATASection(cdataSection: XCDATASection) throws 125 | 126 | func writeProcessingInstruction(processingInstruction: XProcessingInstruction) throws 127 | 128 | func writeComment(comment: XComment) throws 129 | 130 | func writeInternalEntityDeclaration(internalEntityDeclaration: XInternalEntityDeclaration) throws 131 | 132 | func writeExternalEntityDeclaration(externalEntityDeclaration: XExternalEntityDeclaration) throws 133 | 134 | func writeUnparsedEntityDeclaration(unparsedEntityDeclaration: XUnparsedEntityDeclaration) throws 135 | 136 | func writeNotationDeclaration(notationDeclaration: XNotationDeclaration) throws 137 | 138 | func writeParameterEntityDeclaration(parameterEntityDeclaration: XParameterEntityDeclaration) throws 139 | 140 | func writeInternalEntity(internalEntity: XInternalEntity) throws 141 | 142 | func writeExternalEntity(externalEntity: XExternalEntity) throws 143 | 144 | func writeElementDeclaration(elementDeclaration: XElementDeclaration) throws 145 | 146 | func writeAttributeListDeclaration(attributeListDeclaration: XAttributeListDeclaration) throws 147 | 148 | func writeDocumentEnd(document: XDocument) throws 149 | } 150 | 151 | public class DefaultProductionTemplate: XProductionTemplate { 152 | 153 | public let writeEmptyTags: Bool 154 | public let linebreak: String 155 | public let escapeGreaterThan: Bool 156 | public let escapeAllInText: Bool 157 | public let escapeAll: Bool 158 | 159 | 160 | public init( 161 | writeEmptyTags: Bool = true, 162 | escapeGreaterThan: Bool = false, 163 | escapeAllInText: Bool = false, 164 | escapeAll: Bool = false, 165 | linebreak: String = X_DEFAULT_LINEBREAK, 166 | 167 | ) { 168 | self.writeEmptyTags = writeEmptyTags 169 | self.linebreak = linebreak 170 | self.escapeGreaterThan = escapeGreaterThan 171 | self.escapeAllInText = escapeAllInText 172 | self.escapeAll = escapeAll 173 | } 174 | 175 | public func activeProduction( 176 | for writer: Writer, 177 | withStartElement startElement: XElement?, 178 | prefixTranslations: [String:String]?, 179 | declarationSupressingNamespaceURIs: [String]? = nil 180 | ) -> XActiveProduction { 181 | ActiveDefaultProduction( 182 | withStartElement: startElement, 183 | writer: writer, 184 | writeEmptyTags: writeEmptyTags, 185 | escapeGreaterThan: escapeGreaterThan, 186 | escapeAllInText: escapeAllInText, 187 | escapeAll: escapeAll, 188 | linebreak: linebreak, 189 | prefixTranslations: prefixTranslations, 190 | suppressDeclarationForNamespaceURIs: declarationSupressingNamespaceURIs 191 | ) 192 | } 193 | 194 | } 195 | 196 | open class ActiveDefaultProduction: XActiveProduction { 197 | 198 | private var writer: Writer 199 | public var ignore: Bool = false 200 | 201 | public func write(_ text: String) throws { 202 | try writer.write(text) 203 | } 204 | 205 | let escapeGreaterThan: Bool 206 | let escapeAllInText: Bool 207 | let escapeAll: Bool 208 | private let writeEmptyTags: Bool 209 | 210 | private let _linebreak: String 211 | 212 | public var linebreak: String { 213 | get { _linebreak } 214 | } 215 | 216 | let startElement: XElement? 217 | 218 | public let prefixTranslations: [String:String]? 219 | public let declarationSupressingNamespaceURIs: [String]? 220 | 221 | public init( 222 | withStartElement startElement: XElement?, 223 | writer: Writer, 224 | writeEmptyTags: Bool = true, 225 | escapeGreaterThan: Bool = false, 226 | escapeAllInText: Bool = false, 227 | escapeAll: Bool = false, 228 | linebreak: String = X_DEFAULT_LINEBREAK, 229 | prefixTranslations: [String:String]?, 230 | suppressDeclarationForNamespaceURIs declarationSupressingNamespaceURIs: [String]? = nil 231 | ) { 232 | self.startElement = startElement 233 | self.writer = writer 234 | self.writeEmptyTags = writeEmptyTags 235 | self.escapeGreaterThan = escapeGreaterThan 236 | self.escapeAllInText = escapeAllInText 237 | self.escapeAll = escapeAll 238 | self._linebreak = linebreak 239 | self.prefixTranslations = prefixTranslations 240 | self.declarationSupressingNamespaceURIs = declarationSupressingNamespaceURIs 241 | } 242 | 243 | private var _declarationInInternalSubsetIndentation = " " 244 | 245 | public var declarationInInternalSubsetIndentation: String { 246 | set { 247 | _declarationInInternalSubsetIndentation = newValue 248 | } 249 | get { 250 | return _declarationInInternalSubsetIndentation 251 | } 252 | } 253 | 254 | open func sortDeclarationsInInternalSubset(document: XDocument) -> [XDeclarationInInternalSubset] { 255 | var sorted = [XDeclarationInInternalSubset]() 256 | for declarations in [ 257 | sortByName(document.internalEntityDeclarations), 258 | sortByName(document.externalEntityDeclarations), 259 | sortByName(document.notationDeclarations), 260 | sortByName(document.unparsedEntityDeclarations), 261 | sortByName(document.elementDeclarations), 262 | sortByName(document.attributeListDeclarations), 263 | sortByName(document.parameterEntityDeclarations) 264 | ] { 265 | for declaration in declarations { 266 | sorted.append(declaration) 267 | } 268 | } 269 | return sorted 270 | } 271 | 272 | open func writeDocumentStart(document: XDocument) throws { 273 | } 274 | 275 | public static func defaultXMLDeclaration(version: String, encoding: String?, standalone: String?, linebreak: String) -> String? { 276 | if version != "1.0" || encoding != nil || standalone != nil { 277 | "\(linebreak)" 278 | } else { 279 | nil 280 | } 281 | } 282 | 283 | open func writeXMLDeclaration(version: String, encoding: String?, standalone: String?) throws { 284 | if let defaultXMLDeclaration = Self.defaultXMLDeclaration(version: version, encoding: encoding, standalone: standalone, linebreak: linebreak) { 285 | try write(defaultXMLDeclaration) 286 | } 287 | } 288 | 289 | open func writeDocumentTypeDeclarationBeforeInternalSubset(type: String, publicID: String?, systemID: String?, hasInternalSubset: Bool) throws { 290 | if publicID != nil || systemID != nil || hasInternalSubset { 291 | try write("\(linebreak)") 294 | } 295 | } 296 | } 297 | 298 | open func writeDocumentTypeDeclarationInternalSubsetStart() throws { 299 | try write("\(linebreak)[\(linebreak)") 300 | } 301 | 302 | open func writeDocumentTypeDeclarationInternalSubsetEnd() throws { 303 | try write("]") 304 | } 305 | 306 | open func writeDocumentTypeDeclarationAfterInternalSubset(hasInternalSubset: Bool) throws { 307 | if hasInternalSubset { 308 | try write(">\(linebreak)") 309 | } 310 | } 311 | 312 | open func writeElementStartBeforeAttributes(element: XElement) throws { 313 | guard !ignore else { return } 314 | if let prefix = element.prefix { 315 | if let prefixTranslations, let translatedPrefix = prefixTranslations[prefix] { 316 | if translatedPrefix.isEmpty { 317 | try write("<\(element.name)") 318 | } else { 319 | try write("<\(translatedPrefix):\(element.name)") 320 | } 321 | } else { 322 | try write("<\(prefix):\(element.name)") 323 | } 324 | } else { 325 | try write("<\(element.name)") 326 | } 327 | } 328 | 329 | open func sortAttributeNames(attributeNames: [String], element: XElement) -> [String] { 330 | return attributeNames 331 | } 332 | 333 | open func writeAttributeValue(name: String, value: String, element: XElement) throws { 334 | guard !ignore else { return } 335 | try write( 336 | ( 337 | escapeAll ? value.escapingAllForXML : 338 | (escapeGreaterThan ? value.escapingDoubleQuotedValueForXML.replacing(">", with: ">") : value.escapingDoubleQuotedValueForXML) 339 | ) 340 | .replacing("\n", with: " ").replacing("\r", with: " ") 341 | ) 342 | } 343 | 344 | open func writeAttribute(name: String, value: String, element: XElement) throws { 345 | guard !ignore else { return } 346 | try write(" \(name)=\"") 347 | try writeAttributeValue(name: name, value: value, element: element) 348 | try write("\"") 349 | } 350 | 351 | open func writeAsEmptyTagIfEmpty(element: XElement) -> Bool { 352 | return writeEmptyTags 353 | } 354 | 355 | open func writeElementStartAfterAttributes(element: XElement) throws { 356 | guard !ignore else { return } 357 | if element === startElement, let document = element.document, !document._prefixToNamespaceURI.isEmpty { 358 | for (prefix, uri) in document._prefixToNamespaceURI.sorted(by: < ) { 359 | if let declarationSupressingNamespaceURIs, declarationSupressingNamespaceURIs.contains(uri) { continue } 360 | let attributeName: String 361 | if let prefixTranslations, let translatedPrefix = prefixTranslations[prefix] { 362 | if translatedPrefix.isEmpty { 363 | attributeName = "xmlns" 364 | } else { 365 | attributeName = "xmlns:\(translatedPrefix)" 366 | } 367 | } else if prefix.isEmpty { 368 | attributeName = "xmlns" 369 | } else { 370 | attributeName = "xmlns:\(prefix)" 371 | } 372 | try write(" \(attributeName)=\"") 373 | try writeAttributeValue(name: attributeName, value: uri, element: element) 374 | try write("\"") 375 | } 376 | } 377 | if element.isEmpty && writeAsEmptyTagIfEmpty(element: element) { 378 | try write("/>") 379 | } 380 | else { 381 | try write(">") 382 | } 383 | } 384 | 385 | open func writeElementEnd(element: XElement) throws { 386 | guard !ignore else { return } 387 | if !(element.isEmpty && writeAsEmptyTagIfEmpty(element: element)) { 388 | if let prefix = element.prefix { 389 | if let prefixTranslations, let translatedPrefix = prefixTranslations[prefix] { 390 | if translatedPrefix.isEmpty { 391 | try write("") 392 | } else { 393 | try write("") 394 | } 395 | } else { 396 | try write("") 397 | } 398 | } else { 399 | try write("") 400 | } 401 | } 402 | } 403 | 404 | open func writeText(text: XText) throws { 405 | guard !ignore else { return } 406 | try write( 407 | escapeAll || escapeAllInText ? text.value.escapingAllForXML : 408 | (escapeGreaterThan ? text.value.escapingForXML.replacing(">", with: ">") : text.value.escapingForXML) 409 | ) 410 | } 411 | 412 | open func writeLiteral(literal: XLiteral) throws { 413 | guard !ignore else { return } 414 | try write(literal._value) 415 | } 416 | 417 | open func writeCDATASection(cdataSection: XCDATASection) throws { 418 | guard !ignore else { return } 419 | try write("") 420 | } 421 | 422 | open func writeProcessingInstruction(processingInstruction: XProcessingInstruction) throws { 423 | guard !ignore else { return } 424 | try write("") 425 | } 426 | 427 | open func writeComment(comment: XComment) throws { 428 | guard !ignore else { return } 429 | try write("") 430 | } 431 | 432 | open func writeInternalEntityDeclaration(internalEntityDeclaration: XInternalEntityDeclaration) throws { 433 | try write("\(declarationInInternalSubsetIndentation)\(linebreak)") 434 | } 435 | 436 | open func writeExternalEntityDeclaration(externalEntityDeclaration: XExternalEntityDeclaration) throws { 437 | try write("\(declarationInInternalSubsetIndentation)\(linebreak)") 438 | } 439 | 440 | open func writeUnparsedEntityDeclaration(unparsedEntityDeclaration: XUnparsedEntityDeclaration) throws { 441 | try write("\(declarationInInternalSubsetIndentation)\(linebreak)") 442 | } 443 | 444 | open func writeNotationDeclaration(notationDeclaration: XNotationDeclaration) throws { 445 | try write("\(declarationInInternalSubsetIndentation)") 446 | } 447 | 448 | open func writeParameterEntityDeclaration(parameterEntityDeclaration: XParameterEntityDeclaration) throws { 449 | try write("\(declarationInInternalSubsetIndentation)") 450 | } 451 | 452 | open func writeInternalEntity(internalEntity: XInternalEntity) throws { 453 | guard !ignore else { return } 454 | try write("&\(internalEntity._name);") 455 | } 456 | 457 | open func writeExternalEntity(externalEntity: XExternalEntity) throws { 458 | guard !ignore else { return } 459 | try write("&\(externalEntity._name);") 460 | } 461 | 462 | open func writeElementDeclaration(elementDeclaration: XElementDeclaration) throws { 463 | try write("\(declarationInInternalSubsetIndentation)\(elementDeclaration._literal)\(linebreak)") 464 | } 465 | 466 | open func writeAttributeListDeclaration(attributeListDeclaration: XAttributeListDeclaration) throws { 467 | try write("\(declarationInInternalSubsetIndentation)\(attributeListDeclaration._literal)\(linebreak)") 468 | } 469 | 470 | open func writeDocumentEnd(document: XDocument) throws { 471 | 472 | } 473 | } 474 | 475 | public class PrettyPrintProductionTemplate: XProductionTemplate { 476 | 477 | public let textAllowedInElementWithName: [String]? 478 | public let writeEmptyTags: Bool 479 | public let indentation: String 480 | public let linebreak: String 481 | 482 | public init( 483 | textAllowedInElementWithName: [String]? = nil, 484 | writeEmptyTags: Bool = true, 485 | indentation: String? = nil, 486 | linebreak: String? = nil 487 | ) { 488 | self.textAllowedInElementWithName = textAllowedInElementWithName 489 | self.writeEmptyTags = writeEmptyTags 490 | self.indentation = indentation ?? X_DEFAULT_INDENTATION 491 | self.linebreak = linebreak ?? X_DEFAULT_LINEBREAK 492 | } 493 | 494 | public func activeProduction( 495 | for writer: Writer, 496 | withStartElement startElement: XElement?, 497 | prefixTranslations: [String:String]?, 498 | declarationSupressingNamespaceURIs: [String]? 499 | ) -> XActiveProduction { 500 | ActivePrettyPrintProduction( 501 | withStartElement: startElement, 502 | writer: writer, 503 | textAllowedInElementWithName: textAllowedInElementWithName, 504 | writeEmptyTags: writeEmptyTags, 505 | indentation: indentation, 506 | linebreak: linebreak, 507 | prefixTranslations: prefixTranslations, 508 | suppressDeclarationForNamespaceURIs: declarationSupressingNamespaceURIs 509 | ) 510 | } 511 | 512 | } 513 | 514 | open class ActivePrettyPrintProduction: ActiveDefaultProduction { 515 | 516 | private let textAllowedInElementWithName: [String]? 517 | private let indentation: String 518 | 519 | public init( 520 | withStartElement startElement: XElement?, 521 | writer: Writer, 522 | textAllowedInElementWithName: [String]? = nil, 523 | writeEmptyTags: Bool = true, 524 | indentation: String = X_DEFAULT_INDENTATION, 525 | escapeGreaterThan: Bool = false, 526 | escapeAllInText: Bool = false, 527 | escapeAll: Bool = false, 528 | linebreak: String = X_DEFAULT_LINEBREAK, 529 | prefixTranslations: [String:String]?, 530 | suppressDeclarationForNamespaceURIs declarationSupressingNamespaceURIs: [String]?, 531 | ) { 532 | self.textAllowedInElementWithName = textAllowedInElementWithName 533 | self.indentation = indentation 534 | super.init( 535 | withStartElement: startElement, 536 | writer: writer, 537 | writeEmptyTags: writeEmptyTags, 538 | escapeGreaterThan: escapeGreaterThan, 539 | escapeAllInText: escapeAllInText, 540 | escapeAll: escapeAll, 541 | linebreak: linebreak, 542 | prefixTranslations: prefixTranslations, 543 | suppressDeclarationForNamespaceURIs: declarationSupressingNamespaceURIs 544 | ) 545 | } 546 | 547 | private var indentationLevel = 0 548 | 549 | private var mixed = [Bool]() 550 | 551 | open func mightHaveMixedContent(element: XElement) -> Bool { 552 | return textAllowedInElementWithName?.contains(element.name) == true || element.content.contains(where: { $0 is XText || $0 is XInternalEntity || $0 is XInternalEntity }) 553 | } 554 | 555 | /// This can be used to suppress the "pretty print" before an element. 556 | public var suppressPrettyPrintBeforeElement = false 557 | public var forcePrettyPrintAtElement = false 558 | 559 | open override func writeElementStartBeforeAttributes(element: XElement) throws { 560 | if forcePrettyPrintAtElement { mixed.append(false) } 561 | if forcePrettyPrintAtElement || (suppressPrettyPrintBeforeElement == false && mixed.last != true) { 562 | if indentationLevel > 0 { 563 | try write(linebreak) 564 | for _ in 1...indentationLevel { 565 | try write(indentation) 566 | } 567 | } 568 | } 569 | try super.writeElementStartBeforeAttributes(element: element) 570 | } 571 | 572 | open override func writeElementStartAfterAttributes(element: XElement) throws { 573 | try super.writeElementStartAfterAttributes(element: element) 574 | if !element.isEmpty { 575 | mixed.append(mixed.last == true || mightHaveMixedContent(element: element)) 576 | indentationLevel += 1 577 | } 578 | } 579 | 580 | open override func writeElementEnd(element: XElement) throws { 581 | if !element.isEmpty { 582 | indentationLevel -= 1 583 | if forcePrettyPrintAtElement || mixed.last != true { 584 | try write(linebreak) 585 | if indentationLevel > 0 { 586 | for _ in 1...indentationLevel { 587 | try write(indentation) 588 | } 589 | } 590 | } 591 | mixed.removeLast() 592 | } 593 | if forcePrettyPrintAtElement { mixed.removeLast() } 594 | try super.writeElementEnd(element: element) 595 | } 596 | } 597 | 598 | public class HTMLProductionTemplate: XProductionTemplate { 599 | 600 | public let indentation: String 601 | public let linebreak: String 602 | public let suppressDocumentTypeDeclaration: Bool 603 | public let writeAsASCII: Bool 604 | public let escapeGreaterThan: Bool 605 | public let escapeAllInText: Bool 606 | public let escapeAll: Bool 607 | public let suppressUncessaryPrettyPrintAtAnchors: Bool 608 | 609 | public init( 610 | indentation: String = X_DEFAULT_INDENTATION, 611 | linebreak: String = X_DEFAULT_LINEBREAK, 612 | suppressDocumentTypeDeclaration: Bool = false, 613 | writeAsASCII: Bool = false, 614 | escapeGreaterThan: Bool = false, 615 | escapeAllInText: Bool = false, 616 | escapeAll: Bool = false, 617 | suppressUncessaryPrettyPrintAtAnchors: Bool = false 618 | ) { 619 | self.indentation = indentation 620 | self.linebreak = linebreak 621 | self.suppressDocumentTypeDeclaration = suppressDocumentTypeDeclaration 622 | self.writeAsASCII = writeAsASCII 623 | self.escapeGreaterThan = escapeGreaterThan 624 | self.escapeAllInText = escapeAllInText 625 | self.escapeAll = escapeAll 626 | self.suppressUncessaryPrettyPrintAtAnchors = suppressUncessaryPrettyPrintAtAnchors 627 | } 628 | 629 | open func activeProduction( 630 | for writer: Writer, 631 | withStartElement startElement: XElement?, 632 | prefixTranslations: [String:String]?, 633 | declarationSupressingNamespaceURIs: [String]? 634 | ) -> XActiveProduction { 635 | ActiveHTMLProduction( 636 | withStartElement: startElement, 637 | writer: writer, 638 | indentation: indentation, 639 | linebreak: linebreak, 640 | suppressDocumentTypeDeclaration: suppressDocumentTypeDeclaration, 641 | writeAsASCII: writeAsASCII, 642 | escapeGreaterThan: escapeGreaterThan, 643 | escapeAllInText: escapeAllInText, 644 | escapeAll: escapeAll, 645 | suppressUncessaryPrettyPrintAtAnchors: suppressUncessaryPrettyPrintAtAnchors, 646 | prefixTranslations: prefixTranslations, 647 | suppressDeclarationForNamespaceURIs: declarationSupressingNamespaceURIs 648 | ) 649 | } 650 | 651 | } 652 | 653 | open class ActiveHTMLProduction: ActivePrettyPrintProduction { 654 | 655 | public var htmlEmptyTags: [String] 656 | public var htmlStrictBlocks: [String] 657 | public var htmlStrictInlines: [String] 658 | public var blockOrInline: [String] 659 | public var suppressDocumentTypeDeclaration: Bool 660 | public let writeAsASCII: Bool 661 | public let suppressUncessaryPrettyPrintAtAnchors: Bool 662 | 663 | public init( 664 | withStartElement startElement: XElement?, 665 | writer: Writer, 666 | indentation: String = X_DEFAULT_INDENTATION, 667 | linebreak: String = X_DEFAULT_LINEBREAK, 668 | suppressDocumentTypeDeclaration: Bool = false, 669 | writeAsASCII: Bool = false, 670 | escapeGreaterThan: Bool = false, 671 | escapeAllInText: Bool = false, 672 | escapeAll: Bool = false, 673 | suppressUncessaryPrettyPrintAtAnchors: Bool = false, 674 | prefixTranslations: [String:String]?, 675 | suppressDeclarationForNamespaceURIs declarationSupressingNamespaceURIs: [String]? 676 | ) { 677 | htmlEmptyTags = [ 678 | "area", 679 | "base", 680 | "br", 681 | "col", 682 | "embed", 683 | "hr", 684 | "img", 685 | "input", 686 | "link", 687 | "meta", 688 | "param", 689 | "source", 690 | "track", 691 | "wbr", 692 | ] 693 | htmlStrictBlocks = [ 694 | "div", 695 | "p", 696 | "table", 697 | "thead", 698 | "tbody", 699 | "tfoot", 700 | "tr", 701 | "th", 702 | "td", 703 | ] 704 | htmlStrictInlines = [ 705 | "abbr", 706 | "acronym", 707 | "b", 708 | "bdo", 709 | "big", 710 | "br", 711 | "cite", 712 | "code", 713 | "dfn", 714 | "em", 715 | "i", 716 | "input", 717 | "kbd", 718 | "output", 719 | "q", 720 | "samp", 721 | "small", 722 | "span", 723 | "strong", 724 | "sub", 725 | "sup", 726 | "time", 727 | "var", 728 | ] 729 | blockOrInline = [ 730 | "a", 731 | "img", 732 | "object", 733 | ] 734 | self.suppressDocumentTypeDeclaration = suppressDocumentTypeDeclaration 735 | self.writeAsASCII = writeAsASCII 736 | self.suppressUncessaryPrettyPrintAtAnchors = suppressUncessaryPrettyPrintAtAnchors 737 | super.init( 738 | withStartElement: startElement, 739 | writer: writer, 740 | writeEmptyTags: false, 741 | indentation: indentation, 742 | escapeGreaterThan: escapeGreaterThan, 743 | escapeAllInText: escapeAllInText, 744 | escapeAll: escapeAll, 745 | linebreak: linebreak, 746 | prefixTranslations: prefixTranslations, 747 | suppressDeclarationForNamespaceURIs: declarationSupressingNamespaceURIs 748 | ) 749 | } 750 | 751 | open override func writeXMLDeclaration(version: String, encoding: String?, standalone: String?) throws { 752 | // do not write the XML declaration for HTML 753 | } 754 | 755 | open override func writeDocumentTypeDeclarationBeforeInternalSubset(type: String, publicID: String?, systemID: String?, hasInternalSubset: Bool) throws { 756 | if !suppressDocumentTypeDeclaration { 757 | try write("\(linebreak)") 764 | } 765 | } 766 | 767 | open override func writeAsEmptyTagIfEmpty(element: XElement) -> Bool { 768 | return htmlEmptyTags.contains(element.name) 769 | } 770 | 771 | private func isStrictlyInline(_ node: XNode?) -> Bool { 772 | guard let node else { return false } 773 | return node is XText || { 774 | if let element = node as? XElement { 775 | return htmlStrictInlines.contains(element.name) 776 | } 777 | else { 778 | return false 779 | } 780 | }() 781 | } 782 | 783 | private func isStrictlyBlock(_ node: XNode?) -> Bool { 784 | guard let node else { return false } 785 | if let element = node as? XElement { 786 | return htmlStrictBlocks.contains(element.name) 787 | } 788 | else { 789 | return false 790 | } 791 | } 792 | 793 | open override func mightHaveMixedContent(element: XElement) -> Bool { 794 | return element.children({ !self.blockOrInline.contains($0.name) }).absent || element.content.contains(where: { isStrictlyInline($0) }) 795 | } 796 | 797 | open func sort(texts: [String], preferring preferred: String) -> [String] { 798 | return texts.sorted { name1, name2 in 799 | if name2 == preferred { 800 | return false 801 | } 802 | else { 803 | return name1 == preferred || name1 < name2 804 | } 805 | } 806 | } 807 | 808 | open override func writeElementStartBeforeAttributes(element: XElement) throws { 809 | let oldSuppressPrettyPrintBeforeElement = suppressPrettyPrintBeforeElement 810 | let oldForcePrettyPrintAtElement = forcePrettyPrintAtElement 811 | suppressPrettyPrintBeforeElement = isStrictlyInline(element) || ( 812 | suppressUncessaryPrettyPrintAtAnchors && element.name == "a" 813 | && !isStrictlyBlock(element.previousTouching) 814 | ) 815 | forcePrettyPrintAtElement = htmlStrictBlocks.contains(element.name) 816 | try super.writeElementStartBeforeAttributes(element: element) 817 | suppressPrettyPrintBeforeElement = oldSuppressPrettyPrintBeforeElement 818 | forcePrettyPrintAtElement = oldForcePrettyPrintAtElement 819 | } 820 | 821 | open override func writeElementEnd(element: XElement) throws { 822 | let oldForcePrettyPrintAtElement = forcePrettyPrintAtElement 823 | forcePrettyPrintAtElement = (element.lastContent as? XElement)?.fullfills{ htmlStrictBlocks.contains($0.name) } == true 824 | try super.writeElementEnd(element: element) 825 | forcePrettyPrintAtElement = oldForcePrettyPrintAtElement 826 | } 827 | 828 | open override func sortAttributeNames(attributeNames: [String], element: XElement) -> [String] { 829 | if element.name == "meta" { 830 | return sort(texts: attributeNames, preferring: "name") 831 | } 832 | else if element.name == "script" { 833 | return sort(texts: attributeNames, preferring: "src") 834 | } 835 | else { 836 | return super.sortAttributeNames(attributeNames: attributeNames, element: element) 837 | } 838 | } 839 | 840 | open override func writeText(text: XText) throws { 841 | var result = (escapeAll || escapeAllInText) ? text._value.escapingAllForXML : text._value.escapingForXML 842 | if escapeGreaterThan { 843 | result = result.replacing(">", with: ">") 844 | } 845 | if writeAsASCII { 846 | result = result.asciiWithXMLCharacterReferences 847 | } 848 | try write(result) 849 | } 850 | 851 | open override func writeAttributeValue(name: String, value: String, element: XElement) throws { 852 | var result = ( 853 | escapeAll ? value.escapingAllForXML : 854 | (escapeGreaterThan ? value.escapingDoubleQuotedValueForXML.replacing(">", with: ">") : value.escapingDoubleQuotedValueForXML) 855 | ) 856 | .replacing("\n", with: " ").replacing("\r", with: " ") 857 | if writeAsASCII { 858 | result = result.asciiWithXMLCharacterReferences 859 | } 860 | try write(result) 861 | } 862 | 863 | } 864 | 865 | fileprivate extension String { 866 | 867 | var asciiWithXMLCharacterReferences: String { 868 | var texts = [String]() 869 | for scalar in self.unicodeScalars { 870 | if scalar.value < 127 { 871 | texts.append(String(scalar)) 872 | } else { 873 | texts.append("&#x\( String(scalar.value, radix: 16, uppercase: true).prepending("0", until: 4));") 874 | } 875 | } 876 | return texts.joined() 877 | } 878 | 879 | func prepending(_ s: Character, until length: Int) -> String { 880 | let missing = length - self.count 881 | if missing > 0 { 882 | return self + String(repeating: s, count: missing) 883 | } else { 884 | return self 885 | } 886 | } 887 | 888 | } 889 | --------------------------------------------------------------------------------