├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── documentation.yml ├── .gitignore ├── Brewfile ├── Brewfile.lock.json ├── LICENSE.md ├── Makefile ├── Modules ├── libxml2-markup.h └── module.modulemap ├── Package.swift ├── Package@swift-5.1.swift ├── Package@swift-5.2.swift ├── README.md ├── Resources └── xhtml11.xsd ├── Sources ├── DOM │ ├── Builders │ │ ├── DOMBuilder.swift │ │ └── StringBuilder.swift │ ├── Comment.swift │ ├── Document.swift │ ├── DocumentFragment.swift │ ├── DocumentType.swift │ ├── Element.swift │ ├── Error.swift │ ├── Extensions │ │ └── String+Extensions.swift │ ├── Node.swift │ ├── ProcessingInstruction.swift │ └── Text.swift ├── HTML │ ├── Document.swift │ ├── DocumentFragment.swift │ ├── Element.swift │ ├── Extensions │ │ └── XPath+Extensions.swift │ ├── HTMLTags.swift │ ├── HTMLTags.swift.gyb │ ├── Node.swift │ └── Parser.swift ├── XInclude │ └── XInclude.swift ├── XML │ ├── CDATA.swift │ ├── Document.swift │ ├── DocumentFragment.swift │ ├── Element.swift │ ├── Extensions │ │ └── XPath+Extensions.swift │ ├── Namespace.swift │ ├── Node.swift │ └── Parser.swift ├── XPath │ ├── Context.swift │ ├── Expression.swift │ ├── NodeSet.swift │ ├── Object.swift │ └── XPath.swift └── XSLT │ └── XSLT.swift └── Tests ├── HTMLTests ├── HTMLBuilderTests.swift └── HTMLTests.swift ├── LinuxMain.swift └── XMLTests ├── XMLBuilderTests.swift └── XMLTests.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | Resources/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | macos: 13 | runs-on: macos-latest 14 | 15 | strategy: 16 | matrix: 17 | xcode: ["12.4", "11.7", "11.3"] 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v1 22 | - name: Install System Dependencies 23 | if: matrix.xcode == '11.3' 24 | run: brew bundle 25 | - name: Build and Test 26 | run: swift test 27 | env: 28 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer 29 | 30 | linux: 31 | runs-on: ubuntu-latest 32 | 33 | strategy: 34 | matrix: 35 | swift: ["5.3", "5.2", "5.1"] 36 | 37 | container: 38 | image: swift:${{ matrix.swift }} 39 | 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v1 43 | - name: Install System Dependencies 44 | run: | 45 | apt-get update 46 | apt-get install -y libxml2-dev 47 | - name: Build and Test 48 | run: swift test --enable-test-discovery 49 | 50 | windows: 51 | runs-on: windows-latest 52 | 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: seanmiddleditch/gha-setup-vsdevenv@master 56 | 57 | # TODO(compnerd) figure out how to build libxml2 58 | - name: Fetch libxml2 59 | run: | 60 | Invoke-WebRequest -Uri "https://artprodeus21.artifacts.visualstudio.com/A8fd008a0-56bc-482c-ba46-67f9425510be/3133d6ab-80a8-4996-ac4f-03df25cd3224/_apis/artifact/cGlwZWxpbmVhcnRpZmFjdDovL2NvbXBuZXJkL3Byb2plY3RJZC8zMTMzZDZhYi04MGE4LTQ5OTYtYWM0Zi0wM2RmMjVjZDMyMjQvYnVpbGRJZC8zNTI5NS9hcnRpZmFjdE5hbWUveG1sMi13aW5kb3dzLXg2NA2/content?format=zip" -OutFile xml-windows-x64.zip 61 | Expand-Archive -Path xml-windows-x64.zip -DestinationPath $env:Temp -Force 62 | Move-Item -Path $env:Temp\xml2-windows-x64\Library -Destination C:\ -Force 63 | 64 | - name: Install swift-5.4 65 | run: | 66 | Install-Binary -Url "https://swift.org/builds/swift-5.4-release/windows10/swift-5.4-RELEASE/swift-5.4-RELEASE-windows10.exe" -Name "installer.exe" -ArgumentList ("-q") 67 | - name: Set Environment Variables 68 | run: | 69 | echo "SDKROOT=C:\Library\Developer\Platforms\Windows.platform\Developer\SDKs\Windows.sdk" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 70 | echo "DEVELOPER_DIR=C:\Library\Developer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 71 | - name: Adjust Paths 72 | run: | 73 | echo "C:\Library\Swift-development\bin;C:\Library\icu-67\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 74 | echo "C:\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 75 | - name: Install Supporting Files 76 | run: | 77 | Copy-Item "$env:SDKROOT\usr\share\ucrt.modulemap" -destination "$env:UniversalCRTSdkDir\Include\$env:UCRTVersion\ucrt\module.modulemap" 78 | Copy-Item "$env:SDKROOT\usr\share\visualc.modulemap" -destination "$env:VCToolsInstallDir\include\module.modulemap" 79 | Copy-Item "$env:SDKROOT\usr\share\visualc.apinotes" -destination "$env:VCToolsInstallDir\include\visualc.apinotes" 80 | Copy-Item "$env:SDKROOT\usr\share\winsdk.modulemap" -destination "$env:UniversalCRTSdkDir\Include\$env:UCRTVersion\um\module.modulemap" 81 | 82 | - name: Build and Test 83 | run: swift test --enable-test-discovery -Xcc -DLIBXML_STATIC -Xcc -IC:\Library\libxml2-development\usr\include -Xcc -IC:\Library\libxml2-development\usr\include\libxml2 -Xlinker -LC:\Library\libxml2-development\usr\lib 84 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - .github/workflows/documentation.yml 9 | - Sources/**.swift 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v1 18 | - name: Generate Documentation 19 | uses: SwiftDocOrg/swift-doc@master 20 | with: 21 | inputs: "Sources" 22 | output: "Documentation" 23 | - name: Upload Documentation to Wiki 24 | uses: SwiftDocOrg/github-wiki-publish-action@v1 25 | with: 26 | path: "Documentation" 27 | env: 28 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm 7 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | tap 'nshipster/formulae' 2 | brew 'gyb' 3 | 4 | brew 'libxml2', link: true 5 | -------------------------------------------------------------------------------- /Brewfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "tap": { 4 | "nshipster/formulae": { 5 | "revision": "c780bc082b3f89580849ba6a457c242ec9158b4a" 6 | } 7 | }, 8 | "brew": { 9 | "gyb": { 10 | "version": "2019-01-18", 11 | "bottle": false 12 | }, 13 | "libxml2": { 14 | "version": "2.9.10_2", 15 | "bottle": { 16 | "rebuild": 0, 17 | "cellar": ":any", 18 | "prefix": "/usr/local", 19 | "root_url": "https://homebrew.bintray.com/bottles", 20 | "files": { 21 | "arm64_big_sur": { 22 | "url": "https://homebrew.bintray.com/bottles/libxml2-2.9.10_2.arm64_big_sur.bottle.tar.gz", 23 | "sha256": "c2e1bb939465a54e70ac4a6a8c333d00bc01a3738037f77cfd2227e47053ff47" 24 | }, 25 | "big_sur": { 26 | "url": "https://homebrew.bintray.com/bottles/libxml2-2.9.10_2.big_sur.bottle.tar.gz", 27 | "sha256": "0170a16da823ce77d1aad7db927b23a1adb12285a174f36a918275d7952eaaae" 28 | }, 29 | "catalina": { 30 | "url": "https://homebrew.bintray.com/bottles/libxml2-2.9.10_2.catalina.bottle.tar.gz", 31 | "sha256": "2983d5a448504389888720bf951713114ed7f010d96cde9289fdc5c4b539d303" 32 | }, 33 | "mojave": { 34 | "url": "https://homebrew.bintray.com/bottles/libxml2-2.9.10_2.mojave.bottle.tar.gz", 35 | "sha256": "7bcd780db5693475c7711eefbbcf703507865e06483e7338ab61027ec375c4bc" 36 | }, 37 | "high_sierra": { 38 | "url": "https://homebrew.bintray.com/bottles/libxml2-2.9.10_2.high_sierra.bottle.tar.gz", 39 | "sha256": "34d84eaef7f80632a6547903d640be06c6d92b9ca2b815b64b74943b4cf73e63" 40 | } 41 | } 42 | }, 43 | "options": { 44 | "link": true 45 | } 46 | } 47 | } 48 | }, 49 | "system": { 50 | "macos": { 51 | "catalina": { 52 | "HOMEBREW_VERSION": "3.0.10-46-g958b2ec", 53 | "HOMEBREW_PREFIX": "/usr/local", 54 | "Homebrew/homebrew-core": "acc12fd244cdc4664a57469a4ee7d2471bfc3f52", 55 | "CLT": "12.4.0.0.1.1610135815", 56 | "Xcode": "12.4", 57 | "macOS": "10.15.7" 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Read Evaluate Press, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | Sources/HTML/HTMLTags.swift: Resources/xhtml11.xsd 2 | 3 | %.swift: %.swift.gyb 4 | @gyb --line-directive '' -o $@ $< 5 | 6 | .PHONY: 7 | clean: 8 | @rm Sources/HTML/HTMLTags.swift 9 | -------------------------------------------------------------------------------- /Modules/libxml2-markup.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBXML2_MARKUP_H 2 | #define LIBXML2_MARKUP_H 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #if defined(_WIN32) 15 | # if defined(LIBXML_STATIC) 16 | # if defined(LIBXML_DEBUG) 17 | # pragma comment(lib, "libxml2sd.lib") 18 | # else 19 | # pragma comment(lib, "libxml2s.lib") 20 | # endif 21 | # else 22 | # if defined(LIBXML_DEBUG) 23 | # pragma comment(lib, "xml2d.lib") 24 | # else 25 | # pragma comment(lib, "xml2.lib") 26 | # endif 27 | # endif 28 | #elif defined(__ELF__) 29 | __asm__ (".section .swift1_autolink_entries,\"a\",@progbits\n" 30 | ".p2align 3\n" 31 | ".L_swift1_autolink_entries:\n" 32 | " .asciz \"-lxml2\"\n" 33 | " .size .L_swift1_autolink_entries, 7\n"); 34 | #elif defined(__wasm__) 35 | #warning WASM autolinking not implemented 36 | #else /* assume MachO */ 37 | __asm__ (".linker_option \"-lxml2\"\n"); 38 | #endif 39 | 40 | #endif /* LIBXML2_MARKUP_H */ 41 | -------------------------------------------------------------------------------- /Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | module libxml2 [system] { 2 | umbrella header "libxml2-markup.h" 3 | export * 4 | module * { export * } 5 | } 6 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | #if os(Windows) 7 | let systemLibraries: [Target] = [ 8 | .systemLibrary( 9 | name: "libxml2", 10 | path: "Modules" 11 | ), 12 | ] 13 | #else 14 | var providers: [SystemPackageProvider] = [.apt(["libxml2-dev"])] 15 | #if swift(<5.2) 16 | providers += [.brew(["libxml2"])] 17 | #endif 18 | let systemLibraries: [Target] = [ 19 | .systemLibrary( 20 | name: "libxml2", 21 | path: "Modules", 22 | pkgConfig: "libxml-2.0", 23 | providers: providers 24 | ) 25 | ] 26 | #endif 27 | 28 | let package = Package( 29 | name: "Markup", 30 | products: [ 31 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 32 | .library( 33 | name: "Markup", 34 | targets: ["XML", "HTML"]), 35 | ], 36 | dependencies: [ 37 | // Dependencies declare other packages that this package depends on. 38 | // .package(url: /* package url */, from: "1.0.0"), 39 | ], 40 | targets: systemLibraries + [ 41 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 42 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 43 | .target( 44 | name: "DOM", 45 | dependencies: ["libxml2"]), 46 | .target( 47 | name: "HTML", 48 | dependencies: ["DOM", "XPath", "libxml2"], 49 | exclude: ["HTMLTags.swift.gyb"]), 50 | .target( 51 | name: "XML", 52 | dependencies: ["DOM", "XPath", "libxml2"]), 53 | .target( 54 | name: "XPath", 55 | dependencies: ["DOM", "libxml2"]), 56 | .target( 57 | name: "XInclude", 58 | dependencies: ["libxml2"]), 59 | .target( 60 | name: "XSLT", 61 | dependencies: ["libxml2"]), 62 | .testTarget( 63 | name: "HTMLTests", 64 | dependencies: ["HTML"]), 65 | .testTarget( 66 | name: "XMLTests", 67 | dependencies: ["XML"]), 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /Package@swift-5.1.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | var providers: [SystemPackageProvider] = [.apt(["libxml2-dev"])] 7 | #if swift(<5.2) 8 | providers += [.brew(["libxml2"])] 9 | #endif 10 | 11 | let package = Package( 12 | name: "Markup", 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "Markup", 17 | targets: ["XML", "HTML"]), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 26 | .systemLibrary( 27 | name: "libxml2", 28 | path: "Modules", 29 | pkgConfig: "libxml-2.0", 30 | providers: providers 31 | ), 32 | .target( 33 | name: "DOM", 34 | dependencies: ["libxml2"]), 35 | .target( 36 | name: "HTML", 37 | dependencies: ["DOM", "XPath", "libxml2"]), 38 | .target( 39 | name: "XML", 40 | dependencies: ["DOM", "XPath", "libxml2"]), 41 | .target( 42 | name: "XPath", 43 | dependencies: ["DOM", "libxml2"]), 44 | .target( 45 | name: "XInclude", 46 | dependencies: ["libxml2"]), 47 | .target( 48 | name: "XSLT", 49 | dependencies: ["libxml2"]), 50 | .testTarget( 51 | name: "HTMLTests", 52 | dependencies: ["HTML"]), 53 | .testTarget( 54 | name: "XMLTests", 55 | dependencies: ["XML"]), 56 | ] 57 | ) 58 | -------------------------------------------------------------------------------- /Package@swift-5.2.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | var providers: [SystemPackageProvider] = [.apt(["libxml2-dev"])] 7 | #if swift(<5.2) 8 | providers += [.brew(["libxml2"])] 9 | #endif 10 | 11 | let package = Package( 12 | name: "Markup", 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "Markup", 17 | targets: ["XML", "HTML"]), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 26 | .systemLibrary( 27 | name: "libxml2", 28 | path: "Modules", 29 | pkgConfig: "libxml-2.0", 30 | providers: providers 31 | ), 32 | .target( 33 | name: "DOM", 34 | dependencies: ["libxml2"]), 35 | .target( 36 | name: "HTML", 37 | dependencies: ["DOM", "XPath", "libxml2"]), 38 | .target( 39 | name: "XML", 40 | dependencies: ["DOM", "XPath", "libxml2"]), 41 | .target( 42 | name: "XPath", 43 | dependencies: ["DOM", "libxml2"]), 44 | .target( 45 | name: "XInclude", 46 | dependencies: ["libxml2"]), 47 | .target( 48 | name: "XSLT", 49 | dependencies: ["libxml2"]), 50 | .testTarget( 51 | name: "HTMLTests", 52 | dependencies: ["HTML"]), 53 | .testTarget( 54 | name: "XMLTests", 55 | dependencies: ["XML"]), 56 | ] 57 | ) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markup 2 | 3 | ![CI][ci badge] 4 | [![Documentation][documentation badge]][documentation] 5 | 6 | A Swift package for working with HTML, XML, and other markup languages, 7 | based on [libxml2][libxml2]. 8 | 9 | **This project is under active development and is not ready for production use.** 10 | 11 | ## Features 12 | 13 | - [x] XML Support 14 | - [x] XHTML4 Support 15 | - [x] XPath Expression Evaluation 16 | - [ ] HTML5 Support (using [Gumbo][gumbo]) 17 | - [ ] CSS Selector to XPath Functionality 18 | - [ ] XML Namespace Support 19 | - [ ] DTD and Relax-NG Validation 20 | - [ ] XInclude Support 21 | - [ ] XSLT Support 22 | - [ ] SAX Parser Interface 23 | - [x] HTML and XML Function Builder Interfaces 24 | 25 | ## Requirements 26 | 27 | - Swift 5.1+ 28 | - [libxml2][libxml2] _(except for macOS with Xcode 11.4 or later)_ 29 | 30 | ## Usage 31 | 32 | ### XML 33 | 34 | #### Parsing & Introspection 35 | 36 | ```swift 37 | import XML 38 | 39 | let xml = #""" 40 | 41 | 42 | Hello! 43 | 44 | """# 45 | 46 | let document = try XML.Document(string: xml)! 47 | document.root?.name // "greeting" 48 | document.root?.content // "Hello!" 49 | 50 | document.children.count // 3 (two comment nodes and one element node) 51 | document.root?.children.count // 1 (one text node) 52 | ``` 53 | 54 | #### Searching and XPath Expression Evaluation 55 | 56 | ```swift 57 | document.search("//greeting").count // 1 58 | document.evaluate("//greeting/text()") // .string("Hello!") 59 | ``` 60 | 61 | #### Modification 62 | 63 | ```swift 64 | for case let comment as Comment in document.children { 65 | comment.remove() 66 | } 67 | 68 | document.root?.name = "valediction" 69 | document.root?["lang"] = "it" 70 | document.root?.content = "Arrivederci!" 71 | 72 | document.description // => 73 | /* 74 | 75 | Arrivederci! 76 | 77 | */ 78 | ``` 79 | 80 | * * * 81 | 82 | ### HTML 83 | 84 | #### Parsing & Introspection 85 | 86 | ```swift 87 | import HTML 88 | 89 | let html = #""" 90 | 91 | 92 | 93 | 94 | 95 | Welcome 96 | 97 | 98 |

Hello, world!

99 | 100 | 101 | """# 102 | 103 | let document = try HTML.Document(string: html)! 104 | document.body?.children.count // 1 (one element node) 105 | document.body?.children.first?.name // "p" 106 | document.body?.children.first?.text // "Hello, world!" 107 | ``` 108 | 109 | #### Searching and XPath Expression Evaluation 110 | 111 | ```swift 112 | document.search("/body/p").count // 1 113 | document.search("/body/p").first?.xpath // "/body/p[0]" 114 | document.evaluate("/body/p/text()") // .string("Hello, world!") 115 | ``` 116 | 117 | #### Creation and Modification 118 | 119 | ```swift 120 | let div = Element(name: "div") 121 | div["class"] = "wrapper" 122 | if let p = document.search("/body/p").first { 123 | p.wrap(inside: div) 124 | } 125 | 126 | document.body?.description // => 127 | /* 128 |
129 |

Hello, world!

130 |
131 | */ 132 | ``` 133 | 134 | #### Builder Interface 135 | 136 | Available in Swift 5.3+. 137 | 138 | ```swift 139 | import HTML 140 | 141 | let document = HTML.Document { 142 | html(["lang": "en"]) { 143 | head { 144 | meta(["charset": "UTF-8"]) 145 | title { "Hello, world!" } 146 | } 147 | 148 | body(["class": "beautiful"]) { 149 | div(["class": "wrapper"]) { 150 | span { "Hello," } 151 | tag("span") { "world!" } 152 | } 153 | } 154 | } 155 | } 156 | 157 | document.description // => 158 | /* 159 | 160 | 161 | 162 | Hello, world! 163 | 164 | 165 | 166 |
167 | Hello, 168 | world! 169 |
170 | 171 | 172 | 173 | 174 | */ 175 | ``` 176 | 177 | ## Installation 178 | 179 | ### Swift Package Manager 180 | 181 | If you're on Linux or if you're on macOS and using Xcode < 11.4, 182 | install the [libxml2][libxml2] system library: 183 | 184 | ```terminal 185 | # macOS for Xcode 11.3 and earlier 186 | $ brew install libxml2 187 | $ brew link --force libxml2 188 | 189 | # Linux (Ubuntu) 190 | $ sudo apt-get install libxml2-dev 191 | ``` 192 | 193 | Add the Markup package to your target dependencies in `Package.swift`: 194 | 195 | ```swift 196 | import PackageDescription 197 | 198 | let package = Package( 199 | name: "YourProject", 200 | dependencies: [ 201 | .package( 202 | url: "https://github.com/SwiftDocOrg/Markup", 203 | from: "0.1.2" 204 | ), 205 | ] 206 | ) 207 | ``` 208 | 209 | Add `Markup` as a dependency to your target(s): 210 | 211 | ```swift 212 | targets: [ 213 | .target( 214 | name: "YourTarget", 215 | dependencies: ["Markup"]), 216 | ``` 217 | 218 | If you're using Markup in an app, 219 | link `libxml2` to your target. 220 | Open your Xcode project (`.xcodeproj`) or workspace (`.xcworkspace`) file, 221 | select your top-level project entry in the Project Navigator, 222 | and select the target using Markup listed under the Targets heading. 223 | Navigate to the "Build Phases" tab, 224 | expand "Link Binary With Libraries", 225 | and click the + button to add a library. 226 | Enter "libxml2" to the search bar, 227 | select "libxml2.tbd" from the filtered list, 228 | and click the Add button. 229 | 230 | Add libxml2 library to your target 231 | 232 | ## License 233 | 234 | MIT 235 | 236 | ## Contact 237 | 238 | Mattt ([@mattt](https://twitter.com/mattt)) 239 | 240 | [libxml2]: http://xmlsoft.org 241 | [gumbo]: https://github.com/google/gumbo-parser 242 | [ci badge]: https://github.com/SwiftDocOrg/Markup/workflows/CI/badge.svg 243 | [documentation badge]: https://github.com/SwiftDocOrg/Markup/workflows/Documentation/badge.svg 244 | [documentation]: https://github.com/SwiftDocOrg/Markup/wiki 245 | -------------------------------------------------------------------------------- /Sources/DOM/Builders/DOMBuilder.swift: -------------------------------------------------------------------------------- 1 | @_functionBuilder 2 | public struct DOMBuilder { 3 | 4 | // MARK: buildExpression 5 | 6 | public static func buildExpression(_ node: Node) -> Node { 7 | return node 8 | } 9 | 10 | public static func buildExpression(_ string: String) -> Node { 11 | return Text(content: string) 12 | } 13 | 14 | // MARK: buildBlock 15 | 16 | public static func buildBlock(_ children: Node...) -> Node { 17 | let fragment = DocumentFragment() 18 | for child in children.compactMap({ $0 }) { 19 | guard !child.isBlank else { continue } 20 | fragment.insert(child: child) 21 | } 22 | 23 | return fragment 24 | } 25 | 26 | public static func buildBlock(_ strings: String...) -> Node { 27 | return Text(content: strings.joined(separator: "\n\n")) 28 | } 29 | 30 | // MARK: buildIf 31 | 32 | public static func buildIf(_ child: Node?) -> Node { 33 | if let child = child { 34 | return child 35 | } else { 36 | return DocumentFragment() 37 | } 38 | } 39 | 40 | public static func buildIf(_ string: String?) -> Node { 41 | return Text(content: string ?? "") 42 | } 43 | 44 | // MARK: buildEither 45 | 46 | public static func buildEither(first: Node) -> Node { 47 | return first 48 | } 49 | 50 | public static func buildEither(second: Node) -> Node { 51 | return second 52 | } 53 | 54 | public static func buildEither(first: String) -> Node { 55 | return Text(content: first) 56 | } 57 | 58 | public static func buildEither(second: String) -> Node { 59 | return Text(content: second) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/DOM/Builders/StringBuilder.swift: -------------------------------------------------------------------------------- 1 | @_functionBuilder 2 | public struct StringBuilder { 3 | // MARK: buildBlock 4 | 5 | public static func buildExpression(_ string: String) -> String { 6 | return string 7 | } 8 | 9 | public static func buildBlock(_ strings: String...) -> String { 10 | return strings.joined(separator: "\n\n") 11 | } 12 | 13 | // MARK: buildIf 14 | 15 | public static func buildIf(_ string: String?) -> String { 16 | return string ?? "" 17 | } 18 | 19 | // MARK: buildEither 20 | 21 | public static func buildEither(first: String) -> String { 22 | return first 23 | } 24 | 25 | public static func buildEither(second: String) -> String { 26 | return second 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/DOM/Comment.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public final class Comment: Node { 4 | public func remove() { 5 | unlink() 6 | } 7 | 8 | public convenience init(content: String) { 9 | let xmlNode = xmlNewComment(content)! 10 | self.init(rawValue: UnsafeMutableRawPointer(xmlNode))! 11 | } 12 | 13 | // MARK: - 14 | 15 | public required init?(rawValue: UnsafeMutableRawPointer) { 16 | guard rawValue.bindMemory(to: _xmlNode.self, capacity: 1).pointee.type == XML_COMMENT_NODE else { return nil } 17 | super.init(rawValue: rawValue) 18 | } 19 | } 20 | 21 | // MARK: - ExpressibleByStringLiteral 22 | 23 | extension Comment: ExpressibleByStringLiteral { 24 | public convenience init(stringLiteral value: String) { 25 | self.init(content: value) 26 | } 27 | } 28 | 29 | // MARK: - StringBuilder 30 | 31 | extension Comment { 32 | public convenience init(@StringBuilder _ builder: () -> String) { 33 | self.init(content: builder()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/DOM/Document.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | import Foundation 3 | 4 | open class Document: Node { 5 | public enum CloningBehavior: Int32 { 6 | case `default` = 0 7 | 8 | /// If recursive, the content tree will be copied too as well as DTD, namespaces and entities. 9 | case recursive = 1 10 | } 11 | 12 | public var type: DocumentType? { 13 | return DocumentType(rawValue: xmlGetIntSubset(xmlDoc)) 14 | } 15 | 16 | public var version: String? { 17 | return String(cString: xmlDoc.pointee.version) 18 | } 19 | 20 | public var encoding: String.Encoding { 21 | let ianaCharacterSetName = String(cString: xmlDoc.pointee.encoding) 22 | return String.Encoding(ianaCharacterSetName: ianaCharacterSetName) 23 | } 24 | 25 | public func insert(child node: Node) { 26 | xmlAddChild(xmlNode, node.xmlNode) 27 | } 28 | 29 | public var root: Element? { 30 | get { 31 | guard let rawValue = xmlDocGetRootElement(xmlDoc) else { return nil } 32 | return Element(rawValue: rawValue) 33 | } 34 | 35 | set { 36 | if let newValue = newValue { 37 | xmlDocSetRootElement(xmlDoc, newValue.xmlNode) 38 | } else { 39 | root?.unlink() 40 | } 41 | } 42 | } 43 | 44 | public func clone(behavior: CloningBehavior = .recursive) throws -> Self { 45 | guard let rawValue = xmlCopyDoc(xmlDoc, behavior.rawValue) else { throw Error.unknown } 46 | return Self(rawValue: UnsafeMutableRawPointer(rawValue))! 47 | } 48 | 49 | // MARK: - 50 | 51 | var xmlDoc: xmlDocPtr { 52 | rawValue.bindMemory(to: _xmlDoc.self, capacity: 1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/DOM/DocumentFragment.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | import Foundation 3 | 4 | public final class DocumentFragment: Node { 5 | public func insert(child node: Node) { 6 | xmlAddChild(xmlNode, node.xmlNode) 7 | } 8 | 9 | // MARK: - 10 | 11 | public convenience init() { 12 | self.init(rawValue: xmlNewDocFragment(nil))! 13 | } 14 | 15 | public required init?(rawValue: UnsafeMutableRawPointer) { 16 | guard rawValue.bindMemory(to: _xmlNode.self, capacity: 1).pointee.type == XML_DOCUMENT_FRAG_NODE else { return nil } 17 | super.init(rawValue: rawValue) 18 | } 19 | } 20 | 21 | // MARK: - DOMBuilder 22 | 23 | extension DocumentFragment { 24 | public convenience init(@DOMBuilder children builder: () -> Node) { 25 | switch builder() { 26 | case let fragment as DocumentFragment: 27 | self.init(fragment)! 28 | case let node: 29 | self.init() 30 | self.insert(child: node) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/DOM/DocumentType.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public final class DocumentType: Node { 4 | public var name: String { 5 | return String(cString: xmlDtd.pointee.name) 6 | } 7 | 8 | public var externalId: String? { 9 | return String(cString: xmlDtd.pointee.ExternalID) 10 | } 11 | 12 | public var systemId: String? { 13 | return String(cString: xmlDtd.pointee.SystemID) 14 | } 15 | 16 | var xmlDtd: xmlDtdPtr { 17 | rawValue.bindMemory(to: _xmlDtd.self, capacity: 1) 18 | } 19 | 20 | public required init?(rawValue: UnsafeMutableRawPointer) { 21 | guard rawValue.bindMemory(to: _xmlNode.self, capacity: 1).pointee.type == XML_DTD_NODE else { return nil } 22 | super.init(rawValue: rawValue) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/DOM/Element.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public final class Element: Node { 4 | public enum CloningBehavior: Int32 { 5 | case `default` = 0 6 | 7 | /// do a recursive copy (properties, namespaces and children when applicable) 8 | case recursive = 1 9 | 10 | /// copy properties and namespaces (when applicable) 11 | case shallow = 2 12 | } 13 | 14 | public var document: Document? { 15 | guard let rawValue = xmlNode.pointee.doc else { return nil } 16 | return Document(rawValue: UnsafeMutableRawPointer(rawValue)) 17 | } 18 | 19 | public var name: String { 20 | get { 21 | return String(cString: xmlNode.pointee.name) 22 | } 23 | 24 | set { 25 | xmlNodeSetName(xmlNode, newValue) 26 | } 27 | } 28 | 29 | public subscript(attribute: String) -> String? { 30 | get { 31 | return String(xmlString: xmlGetProp(xmlNode, attribute)) 32 | } 33 | 34 | set { 35 | if let newValue = newValue { 36 | xmlSetProp(xmlNode, attribute, newValue) 37 | } else { 38 | xmlUnsetProp(xmlNode, attribute) 39 | } 40 | } 41 | } 42 | 43 | public func append(sibling node: Node) { 44 | xmlAddNextSibling(xmlNode, node.xmlNode) 45 | } 46 | 47 | public func prepend(sibling node: Node) { 48 | xmlAddPrevSibling(xmlNode, node.xmlNode) 49 | } 50 | 51 | public func replace(with node: Node) { 52 | xmlReplaceNode(xmlNode, node.xmlNode) 53 | } 54 | 55 | public func insert(child node: Node) { 56 | xmlAddChild(xmlNode, node.xmlNode) 57 | } 58 | 59 | public func wrap(inside element: Element) { 60 | replace(with: element) 61 | element.insert(child: self) 62 | } 63 | 64 | func clone(behavior: CloningBehavior = .recursive) throws -> Self { 65 | guard let rawValue = xmlCopyNode(xmlNode, behavior.rawValue) else { throw Error.unknown } 66 | return Self(rawValue: UnsafeMutableRawPointer(rawValue))! 67 | } 68 | 69 | public func remove() { 70 | unlink() 71 | } 72 | 73 | // MARK: - 74 | 75 | public convenience init(name: String, attributes: [String: String] = [:]) { 76 | let xmlNode = xmlNewNode(nil, name)! 77 | self.init(rawValue: UnsafeMutableRawPointer(xmlNode))! 78 | 79 | for (attribute, value) in attributes { 80 | self[attribute] = value.description 81 | } 82 | } 83 | 84 | // MARK: - 85 | 86 | public required init?(rawValue: UnsafeMutableRawPointer?) { 87 | guard let rawValue = rawValue else { 88 | return nil 89 | } 90 | guard rawValue.bindMemory(to: _xmlNode.self, capacity: 1).pointee.type == XML_ELEMENT_NODE else { return nil } 91 | super.init(rawValue: rawValue) 92 | } 93 | } 94 | 95 | // MARK: - 96 | 97 | extension Constructable where Self: Element { 98 | public var text: String { 99 | return children.compactMap { child in 100 | switch child { 101 | case let element as Self: 102 | return element.text 103 | case let text as Text where !text.isBlank: 104 | return text.content?.trimmingCharacters(in: .whitespacesAndNewlines) 105 | default: 106 | return nil 107 | } 108 | }.joined(separator: " ") 109 | } 110 | } 111 | 112 | // MARK: - DOMBuilder 113 | 114 | extension Element { 115 | public convenience init(name: String, attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) { 116 | self.init(name: name, attributes: attributes) 117 | 118 | switch builder() { 119 | case let fragment as DocumentFragment & Constructable: 120 | for child in fragment.children { 121 | self.insert(child: child) 122 | } 123 | case let node: 124 | self.insert(child: node) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/DOM/Error.swift: -------------------------------------------------------------------------------- 1 | public enum Error: Swift.Error { 2 | case unknown 3 | } 4 | -------------------------------------------------------------------------------- /Sources/DOM/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | extension String { 4 | public init?(xmlString: UnsafeMutablePointer!, freeWhenDone: Bool = true) { 5 | guard let xmlString = xmlString else { return nil } 6 | defer { 7 | if freeWhenDone { 8 | xmlFree(xmlString) 9 | } 10 | } 11 | 12 | self.init(cString: xmlString) 13 | } 14 | } 15 | 16 | // MARK: - 17 | 18 | extension String.Encoding { 19 | init(ianaCharacterSetName: String) { 20 | switch ianaCharacterSetName { 21 | case "us-ascii": self = .ascii 22 | case "iso-2022-jp": self = .iso2022JP 23 | case "iso-8859-1": self = .isoLatin1 24 | case "iso-8859-2": self = .isoLatin2 25 | case "euc-jp": self = .japaneseEUC 26 | case "macintosh": self = .macOSRoman 27 | case "x-nextstep": self = .nextstep 28 | case "cp932": self = .shiftJIS 29 | case "x-mac-symbol": self = .symbol 30 | case "utf-8": self = .utf8 31 | case "utf-16": self = .utf16 32 | case "utf-16be": self = .utf16BigEndian 33 | case "utf-32": self = .utf32 34 | case "utf-32be": self = .utf32BigEndian 35 | case "utf-32le": self = .utf32LittleEndian 36 | case "windows-1250": self = .windowsCP1250 37 | case "windows-1251": self = .windowsCP1251 38 | case "windows-1252": self = .windowsCP1252 39 | case "windows-1253": self = .windowsCP1253 40 | case "windows-1254": self = .windowsCP1254 41 | default: self = .utf8 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/DOM/Node.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | open class Node: RawRepresentable, Equatable, Hashable, CustomStringConvertible { 4 | public enum SpacePreservingBehavior: Int32 { 5 | case `default` = 0 6 | case preserve = 1 7 | } 8 | 9 | public var line: Int { 10 | return numericCast(xmlGetLineNo(xmlNode)) 11 | } 12 | 13 | public var isBlank: Bool { 14 | return xmlIsBlankNode(xmlNode) != 0 15 | } 16 | 17 | public var spacePreservingBehavior: SpacePreservingBehavior? { 18 | return SpacePreservingBehavior(rawValue: xmlNodeGetSpacePreserve(xmlNode)) 19 | } 20 | 21 | public var content: String? { 22 | get { 23 | return String(xmlString: xmlNodeGetContent(xmlNode)) 24 | } 25 | 26 | set { 27 | xmlNodeSetContent(xmlNode, newValue) 28 | } 29 | } 30 | 31 | public var xpath: String? { 32 | guard let cString = xmlGetNodePath(xmlNode) else { return nil } 33 | return String(xmlString: cString) 34 | } 35 | 36 | func unlink() { 37 | xmlUnlinkNode(xmlNode) 38 | } 39 | 40 | // MARK: - RawRepresentable 41 | 42 | public var rawValue: UnsafeMutableRawPointer 43 | 44 | var xmlNode: xmlNodePtr { 45 | rawValue.bindMemory(to: _xmlNode.self, capacity: 1) 46 | } 47 | 48 | public required init?(rawValue: UnsafeMutableRawPointer) { 49 | self.rawValue = rawValue 50 | } 51 | 52 | public convenience init?(_ node: Node?) { 53 | guard let rawValue = node?.rawValue else { return nil } 54 | self.init(rawValue: rawValue) 55 | } 56 | 57 | // MARK: - CustomStringConvertible 58 | 59 | open var description: String { 60 | let buffer = xmlBufferCreate() 61 | defer { xmlBufferFree(buffer) } 62 | 63 | xmlNodeDump(buffer, xmlNode.pointee.doc, xmlNode, 0, 0) 64 | 65 | return String(cString: xmlBufferContent(buffer)) 66 | } 67 | } 68 | 69 | // MARK: - 70 | 71 | public protocol Constructable { 72 | static func construct(with rawValue: xmlNodePtr?) -> Node? 73 | } 74 | 75 | extension Constructable where Self: Node { 76 | 77 | public var children: [Node] { 78 | guard let firstChild = xmlNode.pointee.children else { return [] } 79 | return sequence(first: firstChild, next: { $0.pointee.next }) 80 | .compactMap { Self.construct(with: $0) } 81 | } 82 | 83 | public var firstChildElement: Element? { 84 | return findFirstNode(start: xmlNode.pointee.children, next: { $0.pointee.next }) { node in 85 | node as? Element != nil 86 | } as? Element 87 | } 88 | 89 | public func firstChildElement(named name: String) -> Element? { 90 | return findFirstNode(start: xmlNode.pointee.children, next: { $0.pointee.next }) { node in 91 | (node as? Element)?.name == name 92 | } as? Element 93 | } 94 | 95 | public var lastChildElement: Element? { 96 | return findFirstNode(start: lastChild?.xmlNode, next: { $0.pointee.prev }) { node in 97 | (node as? Element) != nil 98 | } as? Element 99 | } 100 | 101 | public func lastChildElement(named name: String) -> Element? { 102 | return findFirstNode(start: lastChild?.xmlNode, next: { $0.pointee.prev }) { node in 103 | (node as? Element)?.name == name 104 | } as? Element 105 | } 106 | 107 | public var parent: Element? { 108 | return Element(rawValue: xmlNode.pointee.parent) 109 | } 110 | 111 | public var previous: Node? { 112 | return Self.construct(with: xmlNode.pointee.prev) 113 | } 114 | 115 | public var next: Node? { 116 | return Self.construct(with: xmlNode.pointee.next) 117 | } 118 | 119 | public var firstChild: Node? { 120 | return Self.construct(with: xmlNode.pointee.children) 121 | } 122 | 123 | public var lastChild: Node? { 124 | return Self.construct(with: xmlGetLastChild(xmlNode)) 125 | } 126 | 127 | @discardableResult 128 | public func unwrap() -> Node? { 129 | let children = sequence(first: xmlNode.pointee.children, next: { $0.pointee.next }).compactMap { Node(rawValue: $0) } 130 | guard !children.isEmpty else { return nil } 131 | 132 | if let sibling = previous as? Element { 133 | children.forEach { sibling.append(sibling: $0) } 134 | } else if let sibling = next as? Element { 135 | children.forEach { sibling.prepend(sibling: $0) } 136 | } else if let parent = parent { 137 | children.forEach { parent.insert(child: $0) } 138 | } else { 139 | return nil 140 | } 141 | 142 | defer { unlink() } 143 | 144 | return children.first 145 | } 146 | 147 | private func findFirstNode( 148 | start: UnsafeMutablePointer<_xmlNode>?, 149 | next: @escaping (xmlNodePtr) -> xmlNodePtr?, 150 | where predicate: (Node) -> Bool ) -> Node? 151 | { 152 | var n = start 153 | while let each = n { 154 | if let node = Self.construct(with: each) { 155 | if predicate(node) { 156 | return node 157 | } 158 | } 159 | n = next(each) 160 | } 161 | return nil 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Sources/DOM/ProcessingInstruction.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public final class ProcessingInstruction: Node { 4 | public var target: String { 5 | get { 6 | return String(cString: xmlNode.pointee.name) 7 | } 8 | 9 | set { 10 | xmlNodeSetName(xmlNode, newValue) 11 | } 12 | } 13 | 14 | public func remove() { 15 | unlink() 16 | } 17 | 18 | // MARK: - 19 | 20 | public convenience init(target: String, content: String?) { 21 | let xmlNode = xmlNewPI(target, content)! 22 | self.init(rawValue: UnsafeMutableRawPointer(xmlNode))! 23 | } 24 | 25 | // MARK: - 26 | 27 | public required init?(rawValue: UnsafeMutableRawPointer) { 28 | guard rawValue.bindMemory(to: _xmlNode.self, capacity: 1).pointee.type == XML_PI_NODE else { return nil } 29 | super.init(rawValue: rawValue) 30 | } 31 | } 32 | 33 | // MARK: - StringBuilder 34 | 35 | extension ProcessingInstruction { 36 | public convenience init(target: String, @StringBuilder _ builder: () -> String) { 37 | self.init(target: target, content: builder()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/DOM/Text.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public final class Text: Node { 4 | public func merge(with node: Text) throws { 5 | guard let xmlNode = xmlTextMerge(xmlNode, node.xmlNode) else { throw Error.unknown } 6 | 7 | xmlReplaceNode(self.xmlNode, xmlNode) 8 | } 9 | 10 | public func concatenate(_ string: String) throws { 11 | guard xmlTextConcat(xmlNode, string, numericCast(string.lengthOfBytes(using: .utf8))) == 0 else { throw Error.unknown } 12 | } 13 | 14 | public func remove() { 15 | unlink() 16 | } 17 | 18 | public convenience init(content: String) { 19 | self.init(rawValue: UnsafeMutableRawPointer(xmlNewText(content)))! 20 | } 21 | 22 | // MARK: - 23 | 24 | public required init?(rawValue: UnsafeMutableRawPointer) { 25 | guard rawValue.bindMemory(to: _xmlNode.self, capacity: 1).pointee.type == XML_TEXT_NODE else { return nil } 26 | super.init(rawValue: rawValue) 27 | } 28 | } 29 | 30 | // MARK: - ExpressibleByStringLiteral 31 | 32 | extension Text: ExpressibleByStringLiteral { 33 | public convenience init(stringLiteral value: String) { 34 | self.init(content: value) 35 | } 36 | } 37 | 38 | // MARK: - StringBuilder 39 | 40 | extension Text { 41 | public convenience init(@StringBuilder _ builder: () -> String) { 42 | self.init(content: builder()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/HTML/Document.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | import Foundation 3 | @_exported import DOM 4 | @_exported import XPath 5 | 6 | public final class Document: DOM.Document { 7 | public var head: Element? { 8 | get { 9 | return root?.firstChildElement(named: "head") 10 | } 11 | 12 | set { 13 | guard let root = root else { return } 14 | if let newValue = newValue { 15 | if let oldValue = head { 16 | oldValue.replace(with: newValue) 17 | } else { 18 | root.insert(child: newValue) 19 | } 20 | } else { 21 | head?.remove() 22 | } 23 | } 24 | } 25 | 26 | public var body: Element? { 27 | get { 28 | return root?.firstChildElement(named: "body") 29 | } 30 | 31 | set { 32 | guard let root = root else { return } 33 | if let newValue = newValue { 34 | if let oldValue = body { 35 | oldValue.replace(with: newValue) 36 | } else { 37 | root.insert(child: newValue) 38 | } 39 | } else { 40 | body?.remove() 41 | } 42 | } 43 | } 44 | 45 | public var title: String? { 46 | get { 47 | return head?.text.trimmingCharacters(in: .whitespacesAndNewlines) 48 | } 49 | 50 | set { 51 | head?.content = newValue 52 | } 53 | } 54 | 55 | // MARK: - 56 | 57 | public convenience init?() { 58 | guard let xmlDoc = htmlNewDocNoDtD(nil, nil) else { return nil } 59 | self.init(rawValue: UnsafeMutableRawPointer(xmlDoc))! 60 | } 61 | 62 | // MARK: - RawRepresentable 63 | 64 | var htmlDoc: htmlDocPtr { 65 | rawValue.bindMemory(to: _xmlDoc.self, capacity: 1) 66 | } 67 | 68 | public required init?(rawValue: UnsafeMutableRawPointer) { 69 | guard rawValue.bindMemory(to: _xmlDoc.self, capacity: 1).pointee.type == XML_HTML_DOCUMENT_NODE else { return nil } 70 | super.init(rawValue: rawValue) 71 | } 72 | 73 | // MARK: - CustomStringConvertible 74 | 75 | public override var description: String { 76 | let buffer = xmlBufferCreate() 77 | defer { xmlBufferFree(buffer) } 78 | 79 | let output = xmlOutputBufferCreateBuffer(buffer, nil) 80 | defer { xmlOutputBufferClose(output) } 81 | 82 | htmlDocContentDumpFormatOutput(output, htmlDoc, nil, 0) 83 | 84 | return String(cString: xmlOutputBufferGetContent(output)) 85 | } 86 | } 87 | 88 | // MARK: - XPath 89 | 90 | extension Document { 91 | public func search(xpath: String) -> [Element] { 92 | guard case .nodeSet(let nodeSet) = evaluate(xpath: xpath) else { return [] } 93 | return nodeSet.compactMap { Element($0) } 94 | } 95 | 96 | public func evaluate(xpath: String) -> XPath.Object? { 97 | guard let context = xmlXPathNewContext(htmlDoc) else { return nil } 98 | defer { xmlXPathFreeContext(context) } 99 | 100 | guard let object = xmlXPathEvalExpression(xpath, context) else { return nil } 101 | // defer { xmlXPathFreeObject(object) } 102 | 103 | return XPath.Object(rawValue: object) 104 | } 105 | } 106 | 107 | // MARK: - Builder 108 | 109 | extension Document { 110 | public convenience init?(@DOMBuilder builder: () -> Node) { 111 | self.init() 112 | 113 | switch builder() { 114 | case let fragment as DocumentFragment: 115 | for child in fragment.children { 116 | self.insert(child: child) 117 | } 118 | case let node: 119 | self.insert(child: node) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/HTML/DocumentFragment.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | @_exported import DOM 4 | @_exported import XPath 5 | 6 | extension DocumentFragment { 7 | public func search(xpath: XPath.Expression) -> [Element] { 8 | guard case .nodeSet(let nodeSet) = evaluate(xpath: xpath) else { return [] } 9 | return nodeSet.compactMap { Element($0) } 10 | } 11 | 12 | public func evaluate(xpath: XPath.Expression) -> XPath.Object? { 13 | guard let context = Context(fragment: self) else { return nil } 14 | 15 | guard let object = xmlXPathCompiledEval(xpath.rawValue, context.rawValue) else { return nil } 16 | // defer { xmlXPathFreeObject(object) } 17 | 18 | return XPath.Object(rawValue: object) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/HTML/Element.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | @_exported import DOM 3 | 4 | // MARK: - XPath 5 | 6 | extension Element { 7 | public func search(xpath: XPath.Expression) -> [Element] { 8 | guard case .nodeSet(let nodeSet) = evaluate(xpath: xpath) else { return [] } 9 | return nodeSet.compactMap { Element($0) } 10 | } 11 | 12 | public func evaluate(xpath: XPath.Expression) -> XPath.Object? { 13 | guard let context = Context(element: self) else { return nil } 14 | 15 | guard let object = xmlXPathCompiledEval(xpath.rawValue, context.rawValue) else { return nil } 16 | // defer { xmlXPathFreeObject(object) } 17 | 18 | return XPath.Object(rawValue: object) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/HTML/Extensions/XPath+Extensions.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | import DOM 4 | import XPath 5 | 6 | extension XPath.NodeSet: RandomAccessCollection { 7 | public subscript(position: Int) -> Node? { 8 | precondition(position >= startIndex && position <= endIndex) 9 | guard let rawValue = rawValue.pointee.nodeTab[position] else { return nil } 10 | return Node.construct(with: rawValue) 11 | } 12 | 13 | public var startIndex: Int { 14 | return 0 15 | } 16 | 17 | public var endIndex: Int { 18 | return Int(rawValue.pointee.nodeNr) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/HTML/HTMLTags.swift: -------------------------------------------------------------------------------- 1 | // This file was automatically generated and should not be edited. 2 | 3 | import DOM 4 | 5 | public func tag(_ name: String, _ attributes: [String: String] = [:], @DOMBuilder _ builder: () -> Node = { DocumentFragment() }) -> Element { 6 | return Element(name: name, attributes: attributes, children: builder) 7 | } 8 | 9 | public func a(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 10 | return tag("a", attributes, builder) 11 | } 12 | 13 | public func abbr(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 14 | return tag("abbr", attributes, builder) 15 | } 16 | 17 | public func acronym(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 18 | return tag("acronym", attributes, builder) 19 | } 20 | 21 | public func address(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 22 | return tag("address", attributes, builder) 23 | } 24 | 25 | public func area(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 26 | return tag("area", attributes, builder) 27 | } 28 | 29 | public func b(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 30 | return tag("b", attributes, builder) 31 | } 32 | 33 | public func base(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 34 | return tag("base", attributes, builder) 35 | } 36 | 37 | public func bdo(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 38 | return tag("bdo", attributes, builder) 39 | } 40 | 41 | public func big(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 42 | return tag("big", attributes, builder) 43 | } 44 | 45 | public func blockquote(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 46 | return tag("blockquote", attributes, builder) 47 | } 48 | 49 | public func body(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 50 | return tag("body", attributes, builder) 51 | } 52 | 53 | public func br(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 54 | return tag("br", attributes, builder) 55 | } 56 | 57 | public func button(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 58 | return tag("button", attributes, builder) 59 | } 60 | 61 | public func caption(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 62 | return tag("caption", attributes, builder) 63 | } 64 | 65 | public func cite(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 66 | return tag("cite", attributes, builder) 67 | } 68 | 69 | public func code(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 70 | return tag("code", attributes, builder) 71 | } 72 | 73 | public func col(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 74 | return tag("col", attributes, builder) 75 | } 76 | 77 | public func colgroup(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 78 | return tag("colgroup", attributes, builder) 79 | } 80 | 81 | public func dd(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 82 | return tag("dd", attributes, builder) 83 | } 84 | 85 | public func del(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 86 | return tag("del", attributes, builder) 87 | } 88 | 89 | public func dfn(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 90 | return tag("dfn", attributes, builder) 91 | } 92 | 93 | public func div(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 94 | return tag("div", attributes, builder) 95 | } 96 | 97 | public func dl(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 98 | return tag("dl", attributes, builder) 99 | } 100 | 101 | public func dt(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 102 | return tag("dt", attributes, builder) 103 | } 104 | 105 | public func em(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 106 | return tag("em", attributes, builder) 107 | } 108 | 109 | public func fieldset(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 110 | return tag("fieldset", attributes, builder) 111 | } 112 | 113 | public func form(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 114 | return tag("form", attributes, builder) 115 | } 116 | 117 | public func h1(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 118 | return tag("h1", attributes, builder) 119 | } 120 | 121 | public func h2(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 122 | return tag("h2", attributes, builder) 123 | } 124 | 125 | public func h3(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 126 | return tag("h3", attributes, builder) 127 | } 128 | 129 | public func h4(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 130 | return tag("h4", attributes, builder) 131 | } 132 | 133 | public func h5(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 134 | return tag("h5", attributes, builder) 135 | } 136 | 137 | public func h6(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 138 | return tag("h6", attributes, builder) 139 | } 140 | 141 | public func head(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 142 | return tag("head", attributes, builder) 143 | } 144 | 145 | public func hr(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 146 | return tag("hr", attributes, builder) 147 | } 148 | 149 | public func html(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 150 | return tag("html", attributes, builder) 151 | } 152 | 153 | public func i(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 154 | return tag("i", attributes, builder) 155 | } 156 | 157 | public func img(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 158 | return tag("img", attributes, builder) 159 | } 160 | 161 | public func input(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 162 | return tag("input", attributes, builder) 163 | } 164 | 165 | public func ins(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 166 | return tag("ins", attributes, builder) 167 | } 168 | 169 | public func kbd(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 170 | return tag("kbd", attributes, builder) 171 | } 172 | 173 | public func label(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 174 | return tag("label", attributes, builder) 175 | } 176 | 177 | public func legend(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 178 | return tag("legend", attributes, builder) 179 | } 180 | 181 | public func li(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 182 | return tag("li", attributes, builder) 183 | } 184 | 185 | public func link(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 186 | return tag("link", attributes, builder) 187 | } 188 | 189 | public func map(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 190 | return tag("map", attributes, builder) 191 | } 192 | 193 | public func meta(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 194 | return tag("meta", attributes, builder) 195 | } 196 | 197 | public func noscript(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 198 | return tag("noscript", attributes, builder) 199 | } 200 | 201 | public func object(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 202 | return tag("object", attributes, builder) 203 | } 204 | 205 | public func ol(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 206 | return tag("ol", attributes, builder) 207 | } 208 | 209 | public func optgroup(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 210 | return tag("optgroup", attributes, builder) 211 | } 212 | 213 | public func option(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 214 | return tag("option", attributes, builder) 215 | } 216 | 217 | public func p(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 218 | return tag("p", attributes, builder) 219 | } 220 | 221 | public func param(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 222 | return tag("param", attributes, builder) 223 | } 224 | 225 | public func pre(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 226 | return tag("pre", attributes, builder) 227 | } 228 | 229 | public func q(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 230 | return tag("q", attributes, builder) 231 | } 232 | 233 | public func rb(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 234 | return tag("rb", attributes, builder) 235 | } 236 | 237 | public func rbc(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 238 | return tag("rbc", attributes, builder) 239 | } 240 | 241 | public func rp(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 242 | return tag("rp", attributes, builder) 243 | } 244 | 245 | public func rt(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 246 | return tag("rt", attributes, builder) 247 | } 248 | 249 | public func rtc(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 250 | return tag("rtc", attributes, builder) 251 | } 252 | 253 | public func ruby(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 254 | return tag("ruby", attributes, builder) 255 | } 256 | 257 | public func samp(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 258 | return tag("samp", attributes, builder) 259 | } 260 | 261 | public func script(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 262 | return tag("script", attributes, builder) 263 | } 264 | 265 | public func select(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 266 | return tag("select", attributes, builder) 267 | } 268 | 269 | public func small(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 270 | return tag("small", attributes, builder) 271 | } 272 | 273 | public func span(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 274 | return tag("span", attributes, builder) 275 | } 276 | 277 | public func strong(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 278 | return tag("strong", attributes, builder) 279 | } 280 | 281 | public func style(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 282 | return tag("style", attributes, builder) 283 | } 284 | 285 | public func sub(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 286 | return tag("sub", attributes, builder) 287 | } 288 | 289 | public func sup(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 290 | return tag("sup", attributes, builder) 291 | } 292 | 293 | public func table(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 294 | return tag("table", attributes, builder) 295 | } 296 | 297 | public func tbody(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 298 | return tag("tbody", attributes, builder) 299 | } 300 | 301 | public func td(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 302 | return tag("td", attributes, builder) 303 | } 304 | 305 | public func textarea(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 306 | return tag("textarea", attributes, builder) 307 | } 308 | 309 | public func tfoot(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 310 | return tag("tfoot", attributes, builder) 311 | } 312 | 313 | public func th(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 314 | return tag("th", attributes, builder) 315 | } 316 | 317 | public func thead(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 318 | return tag("thead", attributes, builder) 319 | } 320 | 321 | public func title(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 322 | return tag("title", attributes, builder) 323 | } 324 | 325 | public func tr(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 326 | return tag("tr", attributes, builder) 327 | } 328 | 329 | public func tt(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 330 | return tag("tt", attributes, builder) 331 | } 332 | 333 | public func ul(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 334 | return tag("ul", attributes, builder) 335 | } 336 | 337 | public func `var`(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 338 | return tag("var", attributes, builder) 339 | } 340 | 341 | -------------------------------------------------------------------------------- /Sources/HTML/HTMLTags.swift.gyb: -------------------------------------------------------------------------------- 1 | % warning = "This file was automatically generated and should not be edited." 2 | // ${warning} 3 | %{ 4 | # encoding=utf8 5 | import sys 6 | reload(sys) 7 | sys.setdefaultencoding('utf8') 8 | }% 9 | 10 | import DOM 11 | 12 | public func tag(_ name: String, _ attributes: [String: String] = [:], @DOMBuilder _ builder: () -> Node = { DocumentFragment() }) -> Element { 13 | return Element(name: name, attributes: attributes, children: builder) 14 | } 15 | 16 | %{ 17 | import os 18 | import re 19 | import xml.etree.ElementTree as ET 20 | 21 | namespaces = {"xs": "http://www.w3.org/2001/XMLSchema"} 22 | tree = ET.parse(os.getcwd() + "/../../Resources/xhtml11.xsd") 23 | }% 24 | % for element in sorted(tree.iterfind(".//xs:element", namespaces), key=lambda rule: rule.get('name')): 25 | % if element.get('name') and element.get('abstract') != 'true': 26 | %{ name = element.get('name') }% 27 | % if name == 'var': 28 | %{ name = '`' + name + '`' }% 29 | % end 30 | public func ${name}(_ attributes: [String: String] = [:], @DOMBuilder children builder: () -> Node = { DocumentFragment() }) -> Element { 31 | return tag("${element.get('name')}", attributes, builder) 32 | } 33 | 34 | % end 35 | % end 36 | -------------------------------------------------------------------------------- /Sources/HTML/Node.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | import DOM 4 | 5 | extension Node: Constructable { 6 | public static func construct(with rawValue: xmlNodePtr?) -> Node? { 7 | guard let rawValue = rawValue else { 8 | return nil 9 | } 10 | 11 | switch rawValue.pointee.type { 12 | case XML_ELEMENT_NODE: 13 | return Element(rawValue: rawValue) 14 | case XML_HTML_DOCUMENT_NODE: 15 | return Document(rawValue: rawValue.pointee.doc) 16 | case XML_TEXT_NODE: 17 | return Text(rawValue: rawValue) 18 | case XML_COMMENT_NODE: 19 | return Comment(rawValue: rawValue) 20 | case XML_PI_NODE: 21 | return ProcessingInstruction(rawValue: rawValue) 22 | default: 23 | return nil 24 | } 25 | } 26 | 27 | var xmlNode: xmlNodePtr { 28 | rawValue.bindMemory(to: _xmlNode.self, capacity: 1) 29 | } 30 | 31 | public var parent: Node? { 32 | return Node.construct(with: xmlNode.pointee.parent) 33 | } 34 | 35 | public var previous: Node? { 36 | return Node.construct(with: xmlNode.pointee.prev) 37 | } 38 | 39 | public var next: Node? { 40 | return Node.construct(with: xmlNode.pointee.next) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/HTML/Parser.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | import Foundation 4 | 5 | public enum Parser { 6 | public struct Options: OptionSet { 7 | public var rawValue: Int32 8 | 9 | /// Relaxed parsing 10 | public static let relaxed = Options(HTML_PARSE_RECOVER) 11 | 12 | /// Do not default a doctype if not found 13 | public static let noDefaultDTD = Options(HTML_PARSE_NODEFDTD) 14 | 15 | /// Suppress errors 16 | public static let suppressErrors = Options(HTML_PARSE_NOERROR) 17 | 18 | /// Suppress warnings 19 | public static let suppressWarnings = Options(HTML_PARSE_NOWARNING) 20 | 21 | /// Pedantic error reporting 22 | public static let pedantic = Options(HTML_PARSE_PEDANTIC) 23 | 24 | /// Remove blank nodes 25 | public static let removeBlankNodes = Options(HTML_PARSE_NOBLANKS) 26 | 27 | /// Forbid network access 28 | public static let forbidNetworkAccess = Options(HTML_PARSE_NONET) 29 | 30 | /// Do not add implied html/body... elements 31 | public static let omitImpliedTags = Options(HTML_PARSE_NOIMPLIED) 32 | 33 | /// Compact small text nodes 34 | public static let compact = Options(HTML_PARSE_COMPACT) 35 | 36 | /// Ignore internal document encoding hint 37 | public static let ignoreDocumentEncodingHint = Options(HTML_PARSE_IGNORE_ENC) 38 | 39 | init(_ xmlParserOption: htmlParserOption) { 40 | self.init(rawValue: numericCast(xmlParserOption.rawValue)) 41 | } 42 | 43 | public init(rawValue: Int32) { 44 | self.rawValue = rawValue 45 | } 46 | } 47 | 48 | static func parse(_ string: String, baseURL url: URL? = nil, options: Options) throws -> xmlDocPtr? { 49 | return string.cString(using: .utf8)?.withUnsafeBufferPointer({ 50 | htmlReadMemory($0.baseAddress, numericCast($0.count), url?.absoluteString, nil, options.rawValue) 51 | }) 52 | } 53 | } 54 | 55 | extension Document { 56 | public convenience init?(string: String, baseURL url: URL? = nil, encoding: String.Encoding = .utf8, options: Parser.Options = [.suppressWarnings, .suppressErrors, .relaxed]) throws { 57 | guard let pointer = try Parser.parse(string, baseURL: url, options: options) else { return nil } 58 | self.init(rawValue: pointer) 59 | } 60 | } 61 | 62 | extension DocumentFragment { 63 | public convenience init?(string: String, options: Parser.Options = [.suppressWarnings, .suppressErrors, .relaxed]) throws { 64 | guard let htmlDoc = try Parser.parse(string, options: options), 65 | let htmlDocFragment = xmlNewDocFragment(htmlDoc) 66 | else { return nil } 67 | 68 | // defer { xmlFree(htmlDocFragment) } 69 | 70 | self.init(rawValue: htmlDocFragment) 71 | } 72 | } 73 | 74 | 75 | // MARK: - 76 | 77 | fileprivate var initialization: Void = { xmlInitParser() }() 78 | -------------------------------------------------------------------------------- /Sources/XInclude/XInclude.swift: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /Sources/XML/CDATA.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public final class CDATA: Node { 4 | public convenience init?(content: String, in document: Document) { 5 | guard let xmlNode = xmlNewCDataBlock(document.xmlDoc, content, numericCast(content.lengthOfBytes(using: .utf8))) else { return nil } 6 | self.init(rawValue: UnsafeMutableRawPointer(xmlNode)) 7 | } 8 | 9 | // MARK: - 10 | 11 | public required init?(rawValue: UnsafeMutableRawPointer) { 12 | guard rawValue.bindMemory(to: _xmlNode.self, capacity: 1).pointee.type == XML_TEXT_NODE else { return nil } 13 | super.init(rawValue: rawValue) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/XML/Document.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | import Foundation 3 | @_exported import DOM 4 | @_exported import XPath 5 | 6 | public final class Document: DOM.Document { 7 | public struct Properties: OptionSet { 8 | public var rawValue: Int32 9 | 10 | /// Document is XML well formed 11 | public static let wellFormed = Properties(XML_DOC_WELLFORMED) 12 | 13 | /// Document is Namespace valid 14 | public static let namespaceValid = Properties(XML_DOC_NSVALID) 15 | 16 | /// DTD validation was successful 17 | public static let dtdValid = Properties(XML_DOC_DTDVALID) 18 | 19 | /// XInclude substitution was done 20 | public static let performedXIncludeSubstitution = Properties(XML_DOC_XINCLUDE) 21 | 22 | /// Document was built using the API and not by parsing an instance 23 | public static let userBuilt = Properties(XML_DOC_USERBUILT) 24 | 25 | /// Built for internal processing 26 | public static let internalProcessing = Properties(XML_DOC_INTERNAL) 27 | 28 | init(_ xmlDocProperties: xmlDocProperties) { 29 | self.init(rawValue: numericCast(xmlDocProperties.rawValue)) 30 | } 31 | 32 | public init(rawValue: Int32) { 33 | self.rawValue = rawValue 34 | } 35 | } 36 | 37 | public var properties: Properties { 38 | return Properties(rawValue: xmlDoc.pointee.properties) 39 | } 40 | 41 | // MARK: - 42 | 43 | public convenience init?(version: String? = nil) { 44 | guard let xmlDoc = xmlNewDoc(version) else { return nil } 45 | self.init(rawValue: UnsafeMutableRawPointer(xmlDoc)) 46 | } 47 | 48 | // MARK: - RawRepresentable 49 | 50 | var xmlDoc: xmlDocPtr { 51 | rawValue.bindMemory(to: _xmlDoc.self, capacity: 1) 52 | } 53 | 54 | public required init?(rawValue: UnsafeMutableRawPointer) { 55 | guard rawValue.bindMemory(to: _xmlDoc.self, capacity: 1).pointee.type == XML_DOCUMENT_NODE else { return nil } 56 | super.init(rawValue: rawValue) 57 | } 58 | 59 | // MARK: - CustomStringConvertible 60 | 61 | public override var description: String { 62 | var buffer: UnsafeMutablePointer? 63 | defer { xmlFree(buffer) } 64 | 65 | xmlDocDumpMemoryEnc(xmlDoc, &buffer, nil, "UTF-8") 66 | 67 | return String(cString: buffer!) 68 | } 69 | } 70 | 71 | // MARK: - XPath 72 | 73 | extension Document { 74 | public func search(xpath: XPath.Expression) -> [Element] { 75 | guard case .nodeSet(let nodeSet) = evaluate(xpath: xpath) else { return [] } 76 | return nodeSet.compactMap { Element($0) } 77 | } 78 | 79 | public func evaluate(xpath: XPath.Expression) -> XPath.Object? { 80 | guard let context = Context(document: self) else { return nil } 81 | for namespace in root?.namespaceDefinitions ?? [] { 82 | context.register(namespace: namespace) 83 | } 84 | 85 | guard let object = xmlXPathCompiledEval(xpath.rawValue, context.rawValue) else { return nil } 86 | // defer { xmlXPathFreeObject(object) } 87 | 88 | return XPath.Object(rawValue: object) 89 | } 90 | } 91 | 92 | // MARK: - Builder 93 | 94 | extension Document { 95 | public convenience init?(@DOMBuilder builder: () -> Node) { 96 | self.init() 97 | 98 | switch builder() { 99 | case let fragment as DocumentFragment & Constructable: 100 | for child in fragment.children { 101 | self.insert(child: child) 102 | } 103 | case let node: 104 | self.insert(child: node) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/XML/DocumentFragment.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | @_exported import DOM 4 | @_exported import XPath 5 | 6 | extension DocumentFragment { 7 | public func search(xpath: XPath.Expression) -> [Element] { 8 | guard case .nodeSet(let nodeSet) = evaluate(xpath: xpath) else { return [] } 9 | return nodeSet.compactMap { Element($0) } 10 | } 11 | 12 | public func evaluate(xpath: XPath.Expression) -> XPath.Object? { 13 | guard let context = Context(fragment: self) else { return nil } 14 | 15 | guard let object = xmlXPathCompiledEval(xpath.rawValue, context.rawValue) else { return nil } 16 | // defer { xmlXPathFreeObject(object) } 17 | 18 | return XPath.Object(rawValue: object) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/XML/Element.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | @_exported import DOM 3 | 4 | extension Element { 5 | public var namespace: Namespace? { 6 | guard let rawValue = xmlNode.pointee.ns else { return nil } 7 | return Namespace(rawValue: rawValue) 8 | } 9 | 10 | public var namespaceDefinitions: [Namespace] { 11 | guard let nsDef = xmlNode.pointee.nsDef else { return [] } 12 | return sequence(first: nsDef, next: { $0.pointee.next }).compactMap { Namespace(rawValue: $0) } 13 | } 14 | 15 | public subscript(attribute: String, namespace: Namespace?) -> String? { 16 | get { 17 | if let namespace = namespace { 18 | return String(xmlString: xmlGetNsProp(xmlNode, attribute, namespace.uri)) 19 | } else { 20 | return String(xmlString: xmlGetNoNsProp(xmlNode, attribute)) 21 | } 22 | } 23 | 24 | set { 25 | if let namespace = namespace { 26 | if let newValue = newValue { 27 | xmlSetNsProp(xmlNode, namespace.rawValue, attribute, newValue) 28 | } else { 29 | xmlUnsetNsProp(xmlNode, namespace.rawValue, attribute) 30 | } 31 | } else { 32 | if let newValue = newValue { 33 | xmlSetProp(xmlNode, attribute, newValue) 34 | } else { 35 | xmlUnsetProp(xmlNode, attribute) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | // MARK: - XPath 43 | 44 | extension Element { 45 | public func search(xpath: XPath.Expression) -> [Element] { 46 | guard case .nodeSet(let nodeSet) = evaluate(xpath: xpath) else { return [] } 47 | return nodeSet.compactMap { Element($0) } 48 | } 49 | 50 | public func evaluate(xpath: XPath.Expression) -> XPath.Object? { 51 | guard let context = Context(element: self) else { return nil } 52 | for namespace in namespaceDefinitions { 53 | context.register(namespace: namespace) 54 | } 55 | 56 | guard let object = xmlXPathCompiledEval(xpath.rawValue, context.rawValue) else { return nil } 57 | // defer { xmlXPathFreeObject(object) } 58 | 59 | return XPath.Object(rawValue: object) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/XML/Extensions/XPath+Extensions.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | import DOM 4 | import XPath 5 | 6 | extension XPath.Context { 7 | public func register(namespace: Namespace) { 8 | xmlXPathRegisterNs(rawValue, namespace.prefix, namespace.uri) 9 | } 10 | } 11 | 12 | extension XPath.NodeSet: RandomAccessCollection { 13 | public subscript(position: Int) -> Node? { 14 | precondition(position >= startIndex && position <= endIndex) 15 | guard let rawValue = rawValue.pointee.nodeTab[position] else { return nil } 16 | return Node.construct(with: rawValue) 17 | } 18 | 19 | public var startIndex: Int { 20 | return 0 21 | } 22 | 23 | public var endIndex: Int { 24 | return Int(rawValue.pointee.nodeNr) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/XML/Namespace.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public final class Namespace: RawRepresentable { 4 | public enum Kind: Hashable { 5 | case local 6 | case global 7 | } 8 | 9 | public var context: Document? { 10 | return Document(rawValue: rawValue.pointee.context) 11 | } 12 | 13 | public var uri: String? { 14 | return String(cString: self.rawValue.pointee.href) 15 | } 16 | 17 | public var prefix: String? { 18 | return String(cString: self.rawValue.pointee.prefix) 19 | } 20 | 21 | // MARK: - RawRepresentable 22 | 23 | public var rawValue: xmlNsPtr 24 | 25 | public required init(rawValue: xmlNsPtr) { 26 | self.rawValue = rawValue 27 | } 28 | } 29 | 30 | // MARK: - 31 | 32 | extension Namespace.Kind: RawRepresentable { 33 | public typealias RawValue = xmlNsType 34 | 35 | public init?(rawValue: xmlNsType) { 36 | switch rawValue { 37 | case XML_NAMESPACE_DECL: 38 | self = .local 39 | default: 40 | self = .global 41 | } 42 | } 43 | 44 | public var rawValue: xmlNsType { 45 | return self == .local ? XML_NAMESPACE_DECL : xmlNsType(rawValue: 0) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/XML/Node.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | import DOM 4 | 5 | extension Node: Constructable { 6 | public static func construct(with rawValue: xmlNodePtr?) -> Node? { 7 | guard let rawValue = rawValue else { 8 | return nil 9 | } 10 | 11 | switch rawValue.pointee.type { 12 | case XML_ELEMENT_NODE: 13 | return Element(rawValue: rawValue) 14 | case XML_DOCUMENT_NODE: 15 | return Document(rawValue: rawValue.pointee.doc) 16 | case XML_TEXT_NODE: 17 | return Text(rawValue: rawValue) 18 | case XML_COMMENT_NODE: 19 | return Comment(rawValue: rawValue) 20 | case XML_PI_NODE: 21 | return ProcessingInstruction(rawValue: rawValue) 22 | case XML_CDATA_SECTION_NODE: 23 | return CDATA(rawValue: rawValue) 24 | default: 25 | return nil 26 | } 27 | } 28 | 29 | var xmlNode: xmlNodePtr { 30 | rawValue.bindMemory(to: _xmlNode.self, capacity: 1) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Sources/XML/Parser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | import libxml2 4 | 5 | public enum Parser { 6 | public struct Options: OptionSet { 7 | public var rawValue: Int32 8 | 9 | /// Relaxed parser 10 | public static let relaxed = Options(XML_PARSE_RECOVER) 11 | 12 | /// Substitute entities. 13 | public static let substituteEntities = Options(XML_PARSE_NOENT) 14 | 15 | /// Load external subset 16 | public static let loadDTDExternalSubset = Options(XML_PARSE_DTDLOAD) 17 | 18 | /// Use default DTD attributes 19 | public static let useDefaultDTDAttributes = Options(XML_PARSE_DTDATTR) 20 | 21 | /// Validate with the DTD 22 | public static let validateDTD = Options(XML_PARSE_DTDVALID) 23 | 24 | /// Suppress errors 25 | public static let suppressErrors = Options(XML_PARSE_NOERROR) 26 | 27 | /// Suppress warnings 28 | public static let suppressWarnings = Options(XML_PARSE_NOWARNING) 29 | 30 | /// Pedantic error reporting 31 | public static let pedantic = Options(XML_PARSE_PEDANTIC) 32 | 33 | /// Remove blank nodes 34 | public static let removeBlankNodes = Options(XML_PARSE_NOBLANKS) 35 | 36 | /// Use the SAX1 interface 37 | public static let useSAX1Interface = Options(XML_PARSE_SAX1) 38 | 39 | /// Implement XInclude substituion 40 | public static let implementXIncludeSubstitution = Options(XML_PARSE_XINCLUDE) 41 | 42 | /// Forbid network access 43 | public static let forbidNetworkAccess = Options(XML_PARSE_NONET) 44 | 45 | /// Do not reuse the context dictionary 46 | public static let noContextDictionaryReuse = Options(XML_PARSE_NODICT) 47 | 48 | /// Remove redundant namespaces declarations 49 | public static let removeRedundantNamespaceDeclarations = Options(XML_PARSE_NSCLEAN) 50 | 51 | /// Merge CDATA as text nodes 52 | public static let mergeCDATA = Options(XML_PARSE_NOCDATA) 53 | 54 | /// Do not generate XINCLUDE START/END nodes 55 | public static let noXIncludeDelimiterNodes = Options(XML_PARSE_NOXINCNODE) 56 | 57 | /// Compact small text nodes. 58 | /// - Warning: Modification of the resulting tree isn't allowed 59 | public static let compact = Options(XML_PARSE_COMPACT) 60 | 61 | /// Do not fixup XINCLUDE xml:base uris 62 | public static let noXIncludeBaseURIFixup = Options(XML_PARSE_NOBASEFIX) 63 | 64 | /// Relax any hardcoded limit from the parser 65 | public static let relaxHardcodedLimits = Options(XML_PARSE_HUGE) 66 | 67 | /// Ignore internal document encoding hint 68 | public static let ignoreEncodingHint = Options(XML_PARSE_IGNORE_ENC) 69 | 70 | /// Store big lines numbers in text PSVI field 71 | public static let bigLineNumbers = Options(XML_PARSE_BIG_LINES) 72 | 73 | init(_ xmlParserOption: xmlParserOption) { 74 | self.init(rawValue: numericCast(xmlParserOption.rawValue)) 75 | } 76 | 77 | public init(rawValue: Int32) { 78 | self.rawValue = rawValue 79 | } 80 | } 81 | 82 | static func parse(_ string: String, baseURL url: URL? = nil, options: Options) throws -> xmlDocPtr? { 83 | return string.cString(using: .utf8)?.withUnsafeBufferPointer({ 84 | xmlReadMemory($0.baseAddress, numericCast($0.count), url?.absoluteString, nil, options.rawValue) 85 | }) 86 | } 87 | } 88 | 89 | extension Document { 90 | public convenience init?(string: String, baseURL url: URL? = nil, encoding: String.Encoding = .utf8, options: Parser.Options = [.suppressWarnings, .suppressErrors, .relaxed]) throws { 91 | guard let xmlDoc = try Parser.parse(string, baseURL: url, options: options) else { return nil } 92 | self.init(rawValue: xmlDoc) 93 | } 94 | } 95 | 96 | extension DocumentFragment { 97 | public convenience init?(string: String, options: Parser.Options = [.suppressWarnings, .suppressErrors, .relaxed]) throws { 98 | guard let xmlDoc = try Parser.parse(string, options: options), 99 | let xmlDocFragment = xmlNewDocFragment(xmlDoc) 100 | else { return nil } 101 | 102 | // defer { xmlFree(xmlDocFragment) } 103 | 104 | self.init(rawValue: xmlDocFragment) 105 | } 106 | } 107 | // MARK: - 108 | 109 | fileprivate var initialization: Void = { xmlInitParser() }() 110 | -------------------------------------------------------------------------------- /Sources/XPath/Context.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | import DOM 4 | 5 | public final class Context: RawRepresentable { 6 | public var rawValue: xmlXPathContextPtr 7 | 8 | public convenience init?(document: Document) { 9 | let xmlDoc = document.rawValue.bindMemory(to: _xmlDoc.self, capacity: 1) 10 | self.init(xmlDoc: xmlDoc) 11 | } 12 | 13 | public convenience init?(fragment: DocumentFragment) { 14 | let xmlNode = fragment.rawValue.bindMemory(to: _xmlNode.self, capacity: 1) 15 | self.init(xmlDoc: xmlNode.pointee.doc) 16 | } 17 | 18 | public convenience init?(element: Element) { 19 | guard let document = element.document else { return nil } 20 | self.init(document: document) 21 | let xmlNode = element.rawValue.bindMemory(to: _xmlNode.self, capacity: 1) 22 | xmlXPathSetContextNode(xmlNode, rawValue) 23 | } 24 | 25 | private convenience init?(xmlDoc: xmlDocPtr) { 26 | guard let context = xmlXPathNewContext(xmlDoc) else { return nil } 27 | // defer { xmlXPathFreeContext(context) } 28 | self.init(rawValue: context) 29 | } 30 | 31 | public required init(rawValue: xmlXPathContextPtr) { 32 | self.rawValue = rawValue 33 | } 34 | 35 | public func evaluate(expression: Expression) -> XPath.Object? { 36 | guard let xmlObject = xmlXPathCompiledEval(expression.rawValue, rawValue) else { return nil } 37 | // defer { xmlXPathFreeObject(xmlObject) } 38 | return XPath.Object(rawValue: xmlObject) 39 | } 40 | 41 | public func test(expression: Expression) throws -> Bool { 42 | switch xmlXPathCompiledEvalToBoolean(expression.rawValue, rawValue) { 43 | case 0: 44 | return false 45 | case 1: 46 | return true 47 | default: 48 | throw Error.unknown 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/XPath/Expression.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public final class Expression: RawRepresentable { 4 | public var rawValue: xmlXPathCompExprPtr 5 | 6 | public required init?(rawValue: xmlXPathCompExprPtr) { 7 | self.rawValue = rawValue 8 | } 9 | 10 | public convenience init?(_ string: String) { 11 | guard let compiledExpression = xmlXPathCompile(string) else { return nil } 12 | // defer { xmlXPathFreeCompExpr(compiledExpression) } 13 | self.init(rawValue: compiledExpression) 14 | } 15 | } 16 | 17 | // MARK: - ExpressibleByStringLiteral 18 | 19 | extension Expression: ExpressibleByStringLiteral { 20 | public convenience init(stringLiteral value: String) { 21 | self.init(value)! 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/XPath/NodeSet.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | import DOM 4 | 5 | public final class NodeSet: RawRepresentable, Hashable { 6 | public var rawValue: xmlNodeSetPtr 7 | 8 | public init(rawValue: xmlNodeSetPtr) { 9 | self.rawValue = rawValue 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Sources/XPath/Object.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | public enum Object: Hashable { 4 | case nodeSet(NodeSet) 5 | case boolean(Bool) 6 | case number(Double) 7 | case string(String) 8 | case undefined 9 | 10 | public init?(rawValue: xmlXPathObjectPtr) { 11 | switch rawValue.pointee.type { 12 | case XPATH_NODESET: 13 | guard let nodesetval = rawValue.pointee.nodesetval else { return nil } 14 | self = .nodeSet(NodeSet(rawValue: nodesetval)) 15 | case XPATH_BOOLEAN: 16 | self = .boolean(rawValue.pointee.boolval == 1) // TODO: formalize in extension 17 | case XPATH_NUMBER: 18 | self = .number(rawValue.pointee.floatval) 19 | case XPATH_STRING: 20 | self = .string(String(cString: rawValue.pointee.stringval)) 21 | default: 22 | self = .undefined 23 | } 24 | } 25 | 26 | // public var XPATH_POINT: xmlXPathObjectType { get } 27 | // public var XPATH_RANGE: xmlXPathObjectType { get } 28 | // public var XPATH_LOCATIONSET: xmlXPathObjectType { get } 29 | // public var XPATH_USERS: xmlXPathObjectType { get } 30 | // public var XPATH_XSLT_TREE: xmlXPathObjectType { get } /* An XSLT value tree, non modifiable */ 31 | } 32 | -------------------------------------------------------------------------------- /Sources/XPath/XPath.swift: -------------------------------------------------------------------------------- 1 | import libxml2 2 | 3 | fileprivate var initialization: Void = { xmlXPathInit() }() 4 | 5 | public enum Error: Swift.Error { 6 | case unknown 7 | } 8 | -------------------------------------------------------------------------------- /Sources/XSLT/XSLT.swift: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /Tests/HTMLTests/HTMLBuilderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | import HTML 4 | 5 | #if swift(>=5.3) 6 | final class HTMLBuilderTests: XCTestCase { 7 | func testBuilderWithInitializers() throws { 8 | let actual = Document { 9 | Element(name: "html", attributes: ["lang": "en"]) { 10 | Element(name: "head") { 11 | Element(name: "meta", attributes: ["charset": "UTF-8"]) 12 | Element(name: "title") { "Hello, world!" } 13 | } 14 | 15 | Element(name: "body", attributes: ["class": "beautiful"]) { 16 | ProcessingInstruction(target: "greeter") { "start" } 17 | 18 | Element(name: "div", attributes: ["class": "wrapper"]) { 19 | Element(name: "span") { "Hello," } 20 | Element(name: "span") { "world!" } 21 | } 22 | 23 | ProcessingInstruction(target: "greeter") { "end" } 24 | } 25 | } 26 | } 27 | 28 | let html = #""" 29 | 30 | 31 | 32 | Hello, world! 33 | 34 | 35 | 36 |
37 | Hello, 38 | world! 39 |
40 | 41 | 42 | 43 | """#.split(separator: "\n", omittingEmptySubsequences: false) 44 | .map{ $0.trimmingCharacters(in: .whitespacesAndNewlines) } 45 | .joined() 46 | 47 | let expected = try HTML.Document(string: html, options: [.noDefaultDTD]) 48 | 49 | XCTAssertEqual(actual?.description, expected?.description) 50 | } 51 | 52 | func testBuilderWithFunctions() throws { 53 | let actual = Document { 54 | html(["lang": "en"]) { 55 | head { 56 | meta(["charset": "UTF-8"]) 57 | title { "Hello, world!" } 58 | } 59 | 60 | body(["class": "beautiful"]) { 61 | ProcessingInstruction(target: "greeter") { "start" } 62 | 63 | div(["class": "wrapper"]) { 64 | span { "Hello," } 65 | tag("span") { "world!" } 66 | } 67 | 68 | ProcessingInstruction(target: "greeter") { "end" } 69 | } 70 | } 71 | } 72 | 73 | let source = #""" 74 | 75 | 76 | 77 | Hello, world! 78 | 79 | 80 | 81 |
82 | Hello, 83 | world! 84 |
85 | 86 | 87 | 88 | 89 | """#.split(separator: "\n", omittingEmptySubsequences: false) 90 | .map{ $0.trimmingCharacters(in: .whitespacesAndNewlines) } 91 | .joined() 92 | 93 | let expected = try HTML.Document(string: source, options: [.noDefaultDTD]) 94 | 95 | XCTAssertEqual(actual?.description, expected?.description) 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /Tests/HTMLTests/HTMLTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | import HTML 4 | 5 | final class HTMLTests: XCTestCase { 6 | func testParse() throws { 7 | let html = #""" 8 | 9 | 10 | 11 | 12 | 13 | Hello, world! 14 | 15 | 16 | 17 |
18 | Hello, 19 | world! 20 |
21 | 22 | 23 | 24 | 25 | """# 26 | 27 | let document = try HTML.Document(string: html) 28 | XCTAssertNotNil(document) 29 | 30 | XCTAssertNotNil(document?.head) 31 | XCTAssertEqual(document?.title, "Hello, world!") 32 | 33 | XCTAssertEqual(document?.head?.children.filter { $0 is Element }.count, 3) 34 | 35 | XCTAssertNotNil(document?.body) 36 | XCTAssertEqual(document?.body?["class"], "beautiful") 37 | 38 | let results = document?.search(xpath: "//span") 39 | XCTAssertEqual(results?.count, 2) 40 | XCTAssertEqual(results?.first?.name, "span") 41 | XCTAssertEqual(results?.first?.text, "Hello,") 42 | 43 | results?.last?.name = "strong" 44 | XCTAssertEqual(results?.last?.name, "strong") 45 | XCTAssertEqual(results?.last?.text, "world!") 46 | 47 | results?.last?.content = "mutable world!" 48 | XCTAssertEqual(results?.last?.text, "mutable world!") 49 | 50 | XCTAssertEqual(results?.first?.parent?.text, "Hello, mutable world!") 51 | 52 | let a = Element(name: "a") 53 | a["href"] = "https://example.com/" 54 | XCTAssertEqual(a["href"], "https://example.com/") 55 | 56 | results?.first?.wrap(inside: a) 57 | XCTAssertEqual(results?.first?.parent, a) 58 | 59 | document?.search(xpath: "//div").first?.unwrap() 60 | XCTAssertEqual(a.parent, document?.body) 61 | 62 | let instructions = document?.body?.children.compactMap { $0 as? ProcessingInstruction } 63 | XCTAssertEqual(instructions?.count, 2) 64 | XCTAssertEqual(instructions?.first?.target, "greeter") 65 | XCTAssertEqual(instructions?.first?.content, "start") 66 | XCTAssertEqual(instructions?.last?.target, "greeter") 67 | XCTAssertEqual(instructions?.last?.content, "end") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | fatalError("Run with `swift test --enable-test-discovery`") 2 | -------------------------------------------------------------------------------- /Tests/XMLTests/XMLBuilderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | import XML 4 | 5 | #if swift(>=5.3) 6 | final class XMLBuilderTests: XCTestCase { 7 | func testBuilder() throws { 8 | let actual = Document { 9 | Comment { "begin greeting" } 10 | Element(name: "greeting") { "Hello!" } 11 | Comment { "end greeting" } 12 | } 13 | 14 | let xml = #""" 15 | 16 | 17 | Hello! 18 | 19 | 20 | """# 21 | 22 | let expected = try XML.Document(string: xml) 23 | 24 | XCTAssertEqual(actual?.description, expected?.description) 25 | } 26 | } 27 | #endif 28 | -------------------------------------------------------------------------------- /Tests/XMLTests/XMLTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | import XML 4 | 5 | final class XMLTests: XCTestCase { 6 | func testParseXML() throws { 7 | let xml = #""" 8 | 9 | 10 | Hello! 11 | 12 | """# 13 | 14 | let document = try XML.Document(string: xml) 15 | XCTAssertNotNil(document) 16 | XCTAssertEqual(document?.properties.contains(.userBuilt), false) 17 | 18 | XCTAssertNotNil(document?.root) 19 | XCTAssertEqual(document?.root?.children.count, 1) 20 | 21 | let children = document?.children.map { $0 } ?? [] 22 | XCTAssertEqual(children.count, 3) 23 | 24 | do { 25 | let comment = children[0] as! Comment 26 | XCTAssertEqual(comment.content?.trimmingCharacters(in: .whitespaces), "begin greeting") 27 | XCTAssertEqual(comment.description, "") 28 | } 29 | 30 | do { 31 | let element = children[1] as! Element 32 | XCTAssertEqual(document?.root, element) 33 | 34 | XCTAssertEqual(element.name, "greeting") 35 | XCTAssertEqual(element.text, "Hello!") 36 | 37 | XCTAssertEqual(element.description, #"Hello!"#) 38 | 39 | element["formality"] = "standard" 40 | XCTAssertEqual(element.description, #"Hello!"#) 41 | 42 | XCTAssertEqual(element.evaluate(xpath: "string(text())"), .string("Hello!")) 43 | } 44 | 45 | do { 46 | let comment = children[2] as! Comment 47 | XCTAssertEqual(comment.content?.trimmingCharacters(in: .whitespaces), "end greeting") 48 | XCTAssertEqual(comment.description, "") 49 | } 50 | } 51 | 52 | func testCreate() throws { 53 | let document = Document()! 54 | XCTAssertEqual(document.properties.contains(.userBuilt), true) 55 | 56 | let element = Element(name: "greeting") 57 | element.content = "Hello!" 58 | element["formality"] = "standard" 59 | document.root = element 60 | 61 | element.prepend(sibling: " begin greeting " as Comment) 62 | element.append(sibling: " end greeting " as Comment) 63 | 64 | let expected: String = #""" 65 | 66 | 67 | Hello! 68 | 69 | 70 | """# 71 | 72 | XCTAssertEqual(document.description, expected) 73 | } 74 | 75 | func testParseXSD() throws { 76 | let xsd = #""" 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | """# 86 | 87 | let document = try XML.Document(string: xsd) 88 | 89 | XCTAssertNotNil(document) 90 | 91 | let results = document?.search(xpath: "//xs:element[not(@abstract = 'true')]") 92 | XCTAssertEqual(results?.count, 1) 93 | XCTAssertEqual(results?.first?["name"], "h1") 94 | } 95 | 96 | func testTraverse() throws { 97 | let xml = #""" 98 | 99 | hello 100 | 101 | 102 | 103 | world 104 | 105 | """# 106 | 107 | let doc = try XML.Document(string: xml, options: [.removeBlankNodes])! 108 | let root = doc.root! 109 | XCTAssertEqual(root.name, "root") 110 | XCTAssertNil(root.next) 111 | XCTAssertNil(root.previous) 112 | XCTAssertEqual(root.firstChildElement?.name, "one") 113 | XCTAssertEqual(root.lastChildElement?.name, "three") 114 | XCTAssertEqual(root.firstChild?.content?.trimmingCharacters(in: .whitespacesAndNewlines), "hello") 115 | XCTAssertEqual(root.lastChild?.content?.trimmingCharacters(in: .whitespacesAndNewlines), "world") 116 | XCTAssertNil(root.firstChild?.firstChild) 117 | XCTAssertNil(root.firstChild?.lastChild) 118 | XCTAssertNil(root.firstChildElement?.firstChild) 119 | XCTAssertNil(root.firstChildElement?.lastChild) 120 | } 121 | 122 | func testParent() throws { 123 | XCTAssertNil(Element(name: "test").parent) 124 | } 125 | 126 | } 127 | --------------------------------------------------------------------------------