├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .swift-version ├── .travis.yml ├── Fuzi.podspec ├── Fuzi.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── Fuzi.xcscheme ├── Fuzi.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── FuziDemo ├── FuziDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── FuziDemo │ ├── Info.plist │ ├── main.swift │ └── nutrition.xml ├── LICENSE ├── Package.swift ├── README-ja.md ├── README-zh.md ├── README.md ├── Sources ├── Document.swift ├── Element.swift ├── Error.swift ├── Fuzi.h ├── Helpers.swift ├── Info.plist ├── Node.swift ├── NodeSet.swift └── Queryable.swift └── Tests ├── AtomTests.swift ├── CSSTests.swift ├── DefaultNamespaceXPathTests.swift ├── HTMLTests.swift ├── Info.plist ├── Resources ├── atom.xml ├── ocf.xml ├── vmap.xml ├── web.html └── xml.xml ├── VMAPTests.swift ├── XMLTests.swift └── XPathFunctionResultTests.swift /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description: 2 | * Expected behaviour: 3 | 4 | * Actual behaviour: 5 | 6 | ### Environment 7 | * Package Manager: 8 | - [ ] Carthage, version: 9 | - [ ] CocoaPods, version: 10 | - [ ] Manually 11 | 12 | * Fuzi version: 13 | * Xcode version: 14 | 15 | ### How to reproduce: 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | .DS_Store 46 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | os: osx 4 | osx_image: xcode10.2 5 | 6 | env: 7 | global: 8 | - XCWORKSPACE="Fuzi.xcworkspace" 9 | - SCHEME="Fuzi" 10 | matrix: 11 | - DESTINATION="platform=macOS,arch=x86_64" SDK="macosx" ACTION="test" 12 | - DESTINATION="platform=iOS Simulator,name=iPhone 7" SDK="iphonesimulator" ACTION="test" 13 | - DESTINATION="platform=tvOS Simulator,name=Apple TV 4K" SDK="appletvsimulator" ACTION="test" 14 | - DESTINATION="name=Apple Watch - 38mm" SDK="watchsimulator" ACTION="build" 15 | 16 | script: 17 | - xcodebuild -workspace "$XCWORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -sdk $SDK $ACTION 18 | -------------------------------------------------------------------------------- /Fuzi.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Fuzi" 3 | s.version = "3.1.3" 4 | s.license = "MIT" 5 | s.summary = "A fast & lightweight XML & HTML parser in Swift with XPath & CSS support" 6 | s.homepage = "https://github.com/cezheng/Fuzi" 7 | s.social_media_url = "https://twitter.com/AdamoCheng" 8 | s.author = { "Ce Zheng" => "cezheng.cs@gmail.com" } 9 | s.source = { :git => "https://github.com/cezheng/Fuzi.git", :tag => s.version } 10 | 11 | # cocoadocs.org might not be working 12 | # s.documentation_url = "http://cezheng.github.io/Fuzi" 13 | 14 | s.ios.deployment_target = "8.0" 15 | s.osx.deployment_target = "10.9" 16 | s.watchos.deployment_target = "2.0" 17 | s.tvos.deployment_target = "9.0" 18 | 19 | s.source_files = "Sources/*.swift" 20 | 21 | s.requires_arc = true 22 | s.library = "xml2" 23 | s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' } 24 | end 25 | -------------------------------------------------------------------------------- /Fuzi.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0C354A9C1D9633720005B0AD /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C354A931D9633720005B0AD /* Document.swift */; }; 11 | 0C354A9D1D9633720005B0AD /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C354A941D9633720005B0AD /* Element.swift */; }; 12 | 0C354A9E1D9633720005B0AD /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C354A951D9633720005B0AD /* Error.swift */; }; 13 | 0C354A9F1D9633720005B0AD /* Fuzi.h in Headers */ = {isa = PBXBuildFile; fileRef = 0C354A961D9633720005B0AD /* Fuzi.h */; settings = {ATTRIBUTES = (Public, ); }; }; 14 | 0C354AA01D9633720005B0AD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C354A971D9633720005B0AD /* Helpers.swift */; }; 15 | 0C354AA21D9633720005B0AD /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C354A991D9633720005B0AD /* Node.swift */; }; 16 | 0C354AA31D9633720005B0AD /* NodeSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C354A9A1D9633720005B0AD /* NodeSet.swift */; }; 17 | 0C354AA41D9633720005B0AD /* Queryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C354A9B1D9633720005B0AD /* Queryable.swift */; }; 18 | 792500BC1BA6CFBB0011671B /* Fuzi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 792500AB1BA6CF6B0011671B /* Fuzi.framework */; }; 19 | 792500C81BA6D06B0011671B /* AtomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799F461B1B90479F00E4CE2C /* AtomTests.swift */; }; 20 | 792500C91BA6D06E0011671B /* XMLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799F46271B904CE500E4CE2C /* XMLTests.swift */; }; 21 | 792500CA1BA6D0700011671B /* HTMLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799F462F1B9454A100E4CE2C /* HTMLTests.swift */; }; 22 | 792500CB1BA6D0730011671B /* CSSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799F46321B9543A800E4CE2C /* CSSTests.swift */; }; 23 | 792500CC1BA6D0750011671B /* DefaultNamespaceXPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799F46341B95A64700E4CE2C /* DefaultNamespaceXPathTests.swift */; }; 24 | 792500CD1BA6D0770011671B /* VMAPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799F46361B95B91D00E4CE2C /* VMAPTests.swift */; }; 25 | 792500CE1BA6D0790011671B /* XPathFunctionResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793BCECE1BA1469D009695D6 /* XPathFunctionResultTests.swift */; }; 26 | 792832E91BA93DC60063CC05 /* atom.xml in Resources */ = {isa = PBXBuildFile; fileRef = 792832DA1BA93DBF0063CC05 /* atom.xml */; }; 27 | 792832EA1BA93DC60063CC05 /* ocf.xml in Resources */ = {isa = PBXBuildFile; fileRef = 792832DB1BA93DBF0063CC05 /* ocf.xml */; }; 28 | 792832EB1BA93DC60063CC05 /* vmap.xml in Resources */ = {isa = PBXBuildFile; fileRef = 792832DC1BA93DBF0063CC05 /* vmap.xml */; }; 29 | 792832EC1BA93DC60063CC05 /* web.html in Resources */ = {isa = PBXBuildFile; fileRef = 792832DD1BA93DBF0063CC05 /* web.html */; }; 30 | 792832ED1BA93DC60063CC05 /* xml.xml in Resources */ = {isa = PBXBuildFile; fileRef = 792832DE1BA93DBF0063CC05 /* xml.xml */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXContainerItemProxy section */ 34 | 792500BD1BA6CFBB0011671B /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = 791391741B53B09F00BC07C3 /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = 792500AA1BA6CF6B0011671B; 39 | remoteInfo = "Fuzi-OSX"; 40 | }; 41 | /* End PBXContainerItemProxy section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | 0C354A931D9633720005B0AD /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; 45 | 0C354A941D9633720005B0AD /* Element.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = ""; }; 46 | 0C354A951D9633720005B0AD /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 47 | 0C354A961D9633720005B0AD /* Fuzi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Fuzi.h; sourceTree = ""; }; 48 | 0C354A971D9633720005B0AD /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 49 | 0C354A981D9633720005B0AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | 0C354A991D9633720005B0AD /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; 51 | 0C354A9A1D9633720005B0AD /* NodeSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeSet.swift; sourceTree = ""; }; 52 | 0C354A9B1D9633720005B0AD /* Queryable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queryable.swift; sourceTree = ""; }; 53 | 7913918E1B53B09F00BC07C3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | 792500AB1BA6CF6B0011671B /* Fuzi.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Fuzi.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 792500B71BA6CFBB0011671B /* FuziTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FuziTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 792832DA1BA93DBF0063CC05 /* atom.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = atom.xml; sourceTree = ""; }; 57 | 792832DB1BA93DBF0063CC05 /* ocf.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = ocf.xml; sourceTree = ""; }; 58 | 792832DC1BA93DBF0063CC05 /* vmap.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = vmap.xml; sourceTree = ""; }; 59 | 792832DD1BA93DBF0063CC05 /* web.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = web.html; sourceTree = ""; }; 60 | 792832DE1BA93DBF0063CC05 /* xml.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = xml.xml; sourceTree = ""; }; 61 | 793BCECE1BA1469D009695D6 /* XPathFunctionResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XPathFunctionResultTests.swift; sourceTree = ""; }; 62 | 799F461B1B90479F00E4CE2C /* AtomTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtomTests.swift; sourceTree = ""; }; 63 | 799F46271B904CE500E4CE2C /* XMLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLTests.swift; sourceTree = ""; }; 64 | 799F462F1B9454A100E4CE2C /* HTMLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLTests.swift; sourceTree = ""; }; 65 | 799F46321B9543A800E4CE2C /* CSSTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSSTests.swift; sourceTree = ""; }; 66 | 799F46341B95A64700E4CE2C /* DefaultNamespaceXPathTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultNamespaceXPathTests.swift; sourceTree = ""; }; 67 | 799F46361B95B91D00E4CE2C /* VMAPTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VMAPTests.swift; sourceTree = ""; }; 68 | /* End PBXFileReference section */ 69 | 70 | /* Begin PBXFrameworksBuildPhase section */ 71 | 792500A71BA6CF6B0011671B /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | 792500B41BA6CFBB0011671B /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | 792500BC1BA6CFBB0011671B /* Fuzi.framework in Frameworks */, 83 | ); 84 | runOnlyForDeploymentPostprocessing = 0; 85 | }; 86 | /* End PBXFrameworksBuildPhase section */ 87 | 88 | /* Begin PBXGroup section */ 89 | 0C354A921D9633720005B0AD /* Sources */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 0C354A931D9633720005B0AD /* Document.swift */, 93 | 0C354A941D9633720005B0AD /* Element.swift */, 94 | 0C354A951D9633720005B0AD /* Error.swift */, 95 | 0C354A961D9633720005B0AD /* Fuzi.h */, 96 | 0C354A971D9633720005B0AD /* Helpers.swift */, 97 | 0C354A991D9633720005B0AD /* Node.swift */, 98 | 0C354A9A1D9633720005B0AD /* NodeSet.swift */, 99 | 0C354A9B1D9633720005B0AD /* Queryable.swift */, 100 | 0C354A981D9633720005B0AD /* Info.plist */, 101 | ); 102 | path = Sources; 103 | sourceTree = ""; 104 | }; 105 | 791391731B53B09F00BC07C3 = { 106 | isa = PBXGroup; 107 | children = ( 108 | 0C354A921D9633720005B0AD /* Sources */, 109 | 7913918B1B53B09F00BC07C3 /* Tests */, 110 | 7913917E1B53B09F00BC07C3 /* Products */, 111 | ); 112 | sourceTree = ""; 113 | }; 114 | 7913917E1B53B09F00BC07C3 /* Products */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 792500AB1BA6CF6B0011671B /* Fuzi.framework */, 118 | 792500B71BA6CFBB0011671B /* FuziTests.xctest */, 119 | ); 120 | name = Products; 121 | sourceTree = ""; 122 | }; 123 | 7913918B1B53B09F00BC07C3 /* Tests */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 792832D91BA93DBF0063CC05 /* Resources */, 127 | 799F461B1B90479F00E4CE2C /* AtomTests.swift */, 128 | 799F46271B904CE500E4CE2C /* XMLTests.swift */, 129 | 799F462F1B9454A100E4CE2C /* HTMLTests.swift */, 130 | 799F46321B9543A800E4CE2C /* CSSTests.swift */, 131 | 799F46341B95A64700E4CE2C /* DefaultNamespaceXPathTests.swift */, 132 | 799F46361B95B91D00E4CE2C /* VMAPTests.swift */, 133 | 793BCECE1BA1469D009695D6 /* XPathFunctionResultTests.swift */, 134 | 7913918E1B53B09F00BC07C3 /* Info.plist */, 135 | ); 136 | path = Tests; 137 | sourceTree = ""; 138 | }; 139 | 792832D91BA93DBF0063CC05 /* Resources */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 792832DA1BA93DBF0063CC05 /* atom.xml */, 143 | 792832DB1BA93DBF0063CC05 /* ocf.xml */, 144 | 792832DC1BA93DBF0063CC05 /* vmap.xml */, 145 | 792832DD1BA93DBF0063CC05 /* web.html */, 146 | 792832DE1BA93DBF0063CC05 /* xml.xml */, 147 | ); 148 | path = Resources; 149 | sourceTree = ""; 150 | }; 151 | /* End PBXGroup section */ 152 | 153 | /* Begin PBXHeadersBuildPhase section */ 154 | 792500A81BA6CF6B0011671B /* Headers */ = { 155 | isa = PBXHeadersBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | 0C354A9F1D9633720005B0AD /* Fuzi.h in Headers */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXHeadersBuildPhase section */ 163 | 164 | /* Begin PBXNativeTarget section */ 165 | 792500AA1BA6CF6B0011671B /* Fuzi */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = 792500B01BA6CF6B0011671B /* Build configuration list for PBXNativeTarget "Fuzi" */; 168 | buildPhases = ( 169 | 792500A61BA6CF6B0011671B /* Sources */, 170 | 792500A71BA6CF6B0011671B /* Frameworks */, 171 | 792500A81BA6CF6B0011671B /* Headers */, 172 | 792500A91BA6CF6B0011671B /* Resources */, 173 | ); 174 | buildRules = ( 175 | ); 176 | dependencies = ( 177 | ); 178 | name = Fuzi; 179 | productName = Fuzi; 180 | productReference = 792500AB1BA6CF6B0011671B /* Fuzi.framework */; 181 | productType = "com.apple.product-type.framework"; 182 | }; 183 | 792500B61BA6CFBB0011671B /* FuziTests */ = { 184 | isa = PBXNativeTarget; 185 | buildConfigurationList = 792500BF1BA6CFBB0011671B /* Build configuration list for PBXNativeTarget "FuziTests" */; 186 | buildPhases = ( 187 | 792500B31BA6CFBB0011671B /* Sources */, 188 | 792500B41BA6CFBB0011671B /* Frameworks */, 189 | 792500B51BA6CFBB0011671B /* Resources */, 190 | ); 191 | buildRules = ( 192 | ); 193 | dependencies = ( 194 | 792500BE1BA6CFBB0011671B /* PBXTargetDependency */, 195 | ); 196 | name = FuziTests; 197 | productName = "Fuzi-OSXTests"; 198 | productReference = 792500B71BA6CFBB0011671B /* FuziTests.xctest */; 199 | productType = "com.apple.product-type.bundle.unit-test"; 200 | }; 201 | /* End PBXNativeTarget section */ 202 | 203 | /* Begin PBXProject section */ 204 | 791391741B53B09F00BC07C3 /* Project object */ = { 205 | isa = PBXProject; 206 | attributes = { 207 | LastSwiftUpdateCheck = 0710; 208 | LastUpgradeCheck = 1220; 209 | ORGANIZATIONNAME = "Ce Zheng"; 210 | TargetAttributes = { 211 | 792500AA1BA6CF6B0011671B = { 212 | CreatedOnToolsVersion = 7.0; 213 | LastSwiftMigration = 0900; 214 | }; 215 | 792500B61BA6CFBB0011671B = { 216 | CreatedOnToolsVersion = 7.0; 217 | LastSwiftMigration = 0800; 218 | }; 219 | }; 220 | }; 221 | buildConfigurationList = 791391771B53B09F00BC07C3 /* Build configuration list for PBXProject "Fuzi" */; 222 | compatibilityVersion = "Xcode 3.2"; 223 | developmentRegion = en; 224 | hasScannedForEncodings = 0; 225 | knownRegions = ( 226 | en, 227 | Base, 228 | ); 229 | mainGroup = 791391731B53B09F00BC07C3; 230 | productRefGroup = 7913917E1B53B09F00BC07C3 /* Products */; 231 | projectDirPath = ""; 232 | projectRoot = ""; 233 | targets = ( 234 | 792500AA1BA6CF6B0011671B /* Fuzi */, 235 | 792500B61BA6CFBB0011671B /* FuziTests */, 236 | ); 237 | }; 238 | /* End PBXProject section */ 239 | 240 | /* Begin PBXResourcesBuildPhase section */ 241 | 792500A91BA6CF6B0011671B /* Resources */ = { 242 | isa = PBXResourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | 792500B51BA6CFBB0011671B /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | 792832ED1BA93DC60063CC05 /* xml.xml in Resources */, 253 | 792832EB1BA93DC60063CC05 /* vmap.xml in Resources */, 254 | 792832EC1BA93DC60063CC05 /* web.html in Resources */, 255 | 792832EA1BA93DC60063CC05 /* ocf.xml in Resources */, 256 | 792832E91BA93DC60063CC05 /* atom.xml in Resources */, 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | }; 260 | /* End PBXResourcesBuildPhase section */ 261 | 262 | /* Begin PBXSourcesBuildPhase section */ 263 | 792500A61BA6CF6B0011671B /* Sources */ = { 264 | isa = PBXSourcesBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | 0C354A9D1D9633720005B0AD /* Element.swift in Sources */, 268 | 0C354AA21D9633720005B0AD /* Node.swift in Sources */, 269 | 0C354AA31D9633720005B0AD /* NodeSet.swift in Sources */, 270 | 0C354A9E1D9633720005B0AD /* Error.swift in Sources */, 271 | 0C354A9C1D9633720005B0AD /* Document.swift in Sources */, 272 | 0C354AA01D9633720005B0AD /* Helpers.swift in Sources */, 273 | 0C354AA41D9633720005B0AD /* Queryable.swift in Sources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | 792500B31BA6CFBB0011671B /* Sources */ = { 278 | isa = PBXSourcesBuildPhase; 279 | buildActionMask = 2147483647; 280 | files = ( 281 | 792500CC1BA6D0750011671B /* DefaultNamespaceXPathTests.swift in Sources */, 282 | 792500C91BA6D06E0011671B /* XMLTests.swift in Sources */, 283 | 792500CD1BA6D0770011671B /* VMAPTests.swift in Sources */, 284 | 792500C81BA6D06B0011671B /* AtomTests.swift in Sources */, 285 | 792500CB1BA6D0730011671B /* CSSTests.swift in Sources */, 286 | 792500CA1BA6D0700011671B /* HTMLTests.swift in Sources */, 287 | 792500CE1BA6D0790011671B /* XPathFunctionResultTests.swift in Sources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | /* End PBXSourcesBuildPhase section */ 292 | 293 | /* Begin PBXTargetDependency section */ 294 | 792500BE1BA6CFBB0011671B /* PBXTargetDependency */ = { 295 | isa = PBXTargetDependency; 296 | target = 792500AA1BA6CF6B0011671B /* Fuzi */; 297 | targetProxy = 792500BD1BA6CFBB0011671B /* PBXContainerItemProxy */; 298 | }; 299 | /* End PBXTargetDependency section */ 300 | 301 | /* Begin XCBuildConfiguration section */ 302 | 7913918F1B53B09F00BC07C3 /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 327 | CLANG_WARN_STRICT_PROTOTYPES = YES; 328 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 329 | CLANG_WARN_UNREACHABLE_CODE = YES; 330 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 331 | COPY_PHASE_STRIP = NO; 332 | CURRENT_PROJECT_VERSION = 1; 333 | DEBUG_INFORMATION_FORMAT = dwarf; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | ENABLE_TESTABILITY = YES; 336 | GCC_C_LANGUAGE_STANDARD = gnu99; 337 | GCC_DYNAMIC_NO_PIC = NO; 338 | GCC_NO_COMMON_BLOCKS = YES; 339 | GCC_OPTIMIZATION_LEVEL = 0; 340 | GCC_PREPROCESSOR_DEFINITIONS = ( 341 | "DEBUG=1", 342 | "$(inherited)", 343 | ); 344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 346 | GCC_WARN_UNDECLARED_SELECTOR = YES; 347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 348 | GCC_WARN_UNUSED_FUNCTION = YES; 349 | GCC_WARN_UNUSED_VARIABLE = YES; 350 | HEADER_SEARCH_PATHS = ( 351 | "$(inherited)", 352 | "$(SDKROOT)/usr/include/libxml2", 353 | "$(SRCROOT)/Fuzi", 354 | ); 355 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 356 | MACOSX_DEPLOYMENT_TARGET = 10.9; 357 | MTL_ENABLE_DEBUG_INFO = YES; 358 | ONLY_ACTIVE_ARCH = YES; 359 | PRODUCT_BUNDLE_IDENTIFIER = me.cezheng.Fuzi; 360 | PRODUCT_NAME = "$(PROJECT_NAME)"; 361 | SUPPORTED_PLATFORMS = "macosx iphoneos appletvos watchos iphonesimulator appletvsimulator watchsimulator"; 362 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 363 | SWIFT_VERSION = 5.0; 364 | TARGETED_DEVICE_FAMILY = "1,2,3,4"; 365 | TVOS_DEPLOYMENT_TARGET = 10.0; 366 | VERSIONING_SYSTEM = "apple-generic"; 367 | VERSION_INFO_PREFIX = ""; 368 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 369 | }; 370 | name = Debug; 371 | }; 372 | 791391901B53B09F00BC07C3 /* Release */ = { 373 | isa = XCBuildConfiguration; 374 | buildSettings = { 375 | ALWAYS_SEARCH_USER_PATHS = NO; 376 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 377 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 378 | CLANG_CXX_LIBRARY = "libc++"; 379 | CLANG_ENABLE_MODULES = YES; 380 | CLANG_ENABLE_OBJC_ARC = YES; 381 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 382 | CLANG_WARN_BOOL_CONVERSION = YES; 383 | CLANG_WARN_COMMA = YES; 384 | CLANG_WARN_CONSTANT_CONVERSION = YES; 385 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 386 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 387 | CLANG_WARN_EMPTY_BODY = YES; 388 | CLANG_WARN_ENUM_CONVERSION = YES; 389 | CLANG_WARN_INFINITE_RECURSION = YES; 390 | CLANG_WARN_INT_CONVERSION = YES; 391 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 392 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 393 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 394 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 395 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 396 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 397 | CLANG_WARN_STRICT_PROTOTYPES = YES; 398 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | COPY_PHASE_STRIP = NO; 402 | CURRENT_PROJECT_VERSION = 1; 403 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 404 | ENABLE_NS_ASSERTIONS = NO; 405 | ENABLE_STRICT_OBJC_MSGSEND = YES; 406 | GCC_C_LANGUAGE_STANDARD = gnu99; 407 | GCC_NO_COMMON_BLOCKS = YES; 408 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 409 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 410 | GCC_WARN_UNDECLARED_SELECTOR = YES; 411 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 412 | GCC_WARN_UNUSED_FUNCTION = YES; 413 | GCC_WARN_UNUSED_VARIABLE = YES; 414 | HEADER_SEARCH_PATHS = ( 415 | "$(inherited)", 416 | "$(SDKROOT)/usr/include/libxml2", 417 | "$(SRCROOT)/Fuzi", 418 | ); 419 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 420 | MACOSX_DEPLOYMENT_TARGET = 10.9; 421 | MTL_ENABLE_DEBUG_INFO = NO; 422 | PRODUCT_BUNDLE_IDENTIFIER = me.cezheng.Fuzi; 423 | PRODUCT_NAME = "$(PROJECT_NAME)"; 424 | SUPPORTED_PLATFORMS = "macosx iphoneos appletvos watchos iphonesimulator appletvsimulator watchsimulator"; 425 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 426 | SWIFT_VERSION = 5.0; 427 | TARGETED_DEVICE_FAMILY = "1,2,3,4"; 428 | TVOS_DEPLOYMENT_TARGET = 10.0; 429 | VALIDATE_PRODUCT = YES; 430 | VERSIONING_SYSTEM = "apple-generic"; 431 | VERSION_INFO_PREFIX = ""; 432 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 433 | }; 434 | name = Release; 435 | }; 436 | 792500B11BA6CF6B0011671B /* Debug */ = { 437 | isa = XCBuildConfiguration; 438 | buildSettings = { 439 | APPLICATION_EXTENSION_API_ONLY = YES; 440 | COMBINE_HIDPI_IMAGES = YES; 441 | DEFINES_MODULE = YES; 442 | DYLIB_COMPATIBILITY_VERSION = 1; 443 | DYLIB_CURRENT_VERSION = 1; 444 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 445 | FRAMEWORK_VERSION = A; 446 | INFOPLIST_FILE = Sources/Info.plist; 447 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 448 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 449 | OTHER_LDFLAGS = "-lxml2"; 450 | SDKROOT = macosx; 451 | SKIP_INSTALL = YES; 452 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 453 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 454 | }; 455 | name = Debug; 456 | }; 457 | 792500B21BA6CF6B0011671B /* Release */ = { 458 | isa = XCBuildConfiguration; 459 | buildSettings = { 460 | APPLICATION_EXTENSION_API_ONLY = YES; 461 | COMBINE_HIDPI_IMAGES = YES; 462 | DEFINES_MODULE = YES; 463 | DYLIB_COMPATIBILITY_VERSION = 1; 464 | DYLIB_CURRENT_VERSION = 1; 465 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 466 | FRAMEWORK_VERSION = A; 467 | INFOPLIST_FILE = Sources/Info.plist; 468 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 469 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 470 | OTHER_LDFLAGS = "-lxml2"; 471 | SDKROOT = macosx; 472 | SKIP_INSTALL = YES; 473 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 474 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 475 | }; 476 | name = Release; 477 | }; 478 | 792500C01BA6CFBB0011671B /* Debug */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 482 | COMBINE_HIDPI_IMAGES = YES; 483 | HEADER_SEARCH_PATHS = ( 484 | "$(inherited)", 485 | "$(SDKROOT)/usr/include/libxml2", 486 | ); 487 | INFOPLIST_FILE = Tests/Info.plist; 488 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 489 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 490 | PRODUCT_BUNDLE_IDENTIFIER = "me.cezheng.Fuzi-OSXTests"; 491 | PRODUCT_NAME = "$(TARGET_NAME)"; 492 | SDKROOT = macosx; 493 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 494 | }; 495 | name = Debug; 496 | }; 497 | 792500C11BA6CFBB0011671B /* Release */ = { 498 | isa = XCBuildConfiguration; 499 | buildSettings = { 500 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 501 | COMBINE_HIDPI_IMAGES = YES; 502 | HEADER_SEARCH_PATHS = ( 503 | "$(inherited)", 504 | "$(SDKROOT)/usr/include/libxml2", 505 | ); 506 | INFOPLIST_FILE = Tests/Info.plist; 507 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 508 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 509 | PRODUCT_BUNDLE_IDENTIFIER = "me.cezheng.Fuzi-OSXTests"; 510 | PRODUCT_NAME = "$(TARGET_NAME)"; 511 | SDKROOT = macosx; 512 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 513 | }; 514 | name = Release; 515 | }; 516 | /* End XCBuildConfiguration section */ 517 | 518 | /* Begin XCConfigurationList section */ 519 | 791391771B53B09F00BC07C3 /* Build configuration list for PBXProject "Fuzi" */ = { 520 | isa = XCConfigurationList; 521 | buildConfigurations = ( 522 | 7913918F1B53B09F00BC07C3 /* Debug */, 523 | 791391901B53B09F00BC07C3 /* Release */, 524 | ); 525 | defaultConfigurationIsVisible = 0; 526 | defaultConfigurationName = Release; 527 | }; 528 | 792500B01BA6CF6B0011671B /* Build configuration list for PBXNativeTarget "Fuzi" */ = { 529 | isa = XCConfigurationList; 530 | buildConfigurations = ( 531 | 792500B11BA6CF6B0011671B /* Debug */, 532 | 792500B21BA6CF6B0011671B /* Release */, 533 | ); 534 | defaultConfigurationIsVisible = 0; 535 | defaultConfigurationName = Release; 536 | }; 537 | 792500BF1BA6CFBB0011671B /* Build configuration list for PBXNativeTarget "FuziTests" */ = { 538 | isa = XCConfigurationList; 539 | buildConfigurations = ( 540 | 792500C01BA6CFBB0011671B /* Debug */, 541 | 792500C11BA6CFBB0011671B /* Release */, 542 | ); 543 | defaultConfigurationIsVisible = 0; 544 | defaultConfigurationName = Release; 545 | }; 546 | /* End XCConfigurationList section */ 547 | }; 548 | rootObject = 791391741B53B09F00BC07C3 /* Project object */; 549 | } 550 | -------------------------------------------------------------------------------- /Fuzi.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Fuzi.xcodeproj/xcshareddata/xcschemes/Fuzi.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Fuzi.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Fuzi.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FuziDemo/FuziDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7932B1151BA7EC67008CA6F5 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7932B1131BA7EC67008CA6F5 /* main.swift */; }; 11 | 7932B15E1BA7FAD4008CA6F5 /* nutrition.xml in Resources */ = {isa = PBXBuildFile; fileRef = 7932B15D1BA7FAD4008CA6F5 /* nutrition.xml */; }; 12 | 7932B1671BA85C9D008CA6F5 /* Fuzi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7932B1661BA85C9D008CA6F5 /* Fuzi.framework */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXFileReference section */ 16 | 7932B1131BA7EC67008CA6F5 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = FuziDemo/main.swift; sourceTree = SOURCE_ROOT; }; 17 | 7932B1141BA7EC67008CA6F5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = FuziDemo/Info.plist; sourceTree = SOURCE_ROOT; }; 18 | 7932B15D1BA7FAD4008CA6F5 /* nutrition.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = nutrition.xml; path = FuziDemo/nutrition.xml; sourceTree = SOURCE_ROOT; }; 19 | 7932B1661BA85C9D008CA6F5 /* Fuzi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Fuzi.framework; path = ../build/Debug/Fuzi.framework; sourceTree = ""; }; 20 | 799EB4DD1BA42D4D00E8F920 /* FuziDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FuziDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | /* End PBXFileReference section */ 22 | 23 | /* Begin PBXFrameworksBuildPhase section */ 24 | 799EB4DA1BA42D4D00E8F920 /* Frameworks */ = { 25 | isa = PBXFrameworksBuildPhase; 26 | buildActionMask = 2147483647; 27 | files = ( 28 | 7932B1671BA85C9D008CA6F5 /* Fuzi.framework in Frameworks */, 29 | ); 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXFrameworksBuildPhase section */ 33 | 34 | /* Begin PBXGroup section */ 35 | 799EB4D41BA42D4D00E8F920 = { 36 | isa = PBXGroup; 37 | children = ( 38 | 7932B1661BA85C9D008CA6F5 /* Fuzi.framework */, 39 | 799EB4DF1BA42D4D00E8F920 /* FuziDemo */, 40 | 799EB4DE1BA42D4D00E8F920 /* Products */, 41 | ); 42 | sourceTree = ""; 43 | }; 44 | 799EB4DE1BA42D4D00E8F920 /* Products */ = { 45 | isa = PBXGroup; 46 | children = ( 47 | 799EB4DD1BA42D4D00E8F920 /* FuziDemo.app */, 48 | ); 49 | name = Products; 50 | sourceTree = ""; 51 | }; 52 | 799EB4DF1BA42D4D00E8F920 /* FuziDemo */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 7932B15D1BA7FAD4008CA6F5 /* nutrition.xml */, 56 | 7932B1131BA7EC67008CA6F5 /* main.swift */, 57 | 7932B1141BA7EC67008CA6F5 /* Info.plist */, 58 | ); 59 | name = FuziDemo; 60 | path = FuzeDemo; 61 | sourceTree = ""; 62 | }; 63 | /* End PBXGroup section */ 64 | 65 | /* Begin PBXNativeTarget section */ 66 | 799EB4DC1BA42D4D00E8F920 /* FuziDemo */ = { 67 | isa = PBXNativeTarget; 68 | buildConfigurationList = 799EB4EA1BA42D4D00E8F920 /* Build configuration list for PBXNativeTarget "FuziDemo" */; 69 | buildPhases = ( 70 | 799EB4D91BA42D4D00E8F920 /* Sources */, 71 | 799EB4DA1BA42D4D00E8F920 /* Frameworks */, 72 | 799EB4DB1BA42D4D00E8F920 /* Resources */, 73 | ); 74 | buildRules = ( 75 | ); 76 | dependencies = ( 77 | ); 78 | name = FuziDemo; 79 | productName = FuzeDemo; 80 | productReference = 799EB4DD1BA42D4D00E8F920 /* FuziDemo.app */; 81 | productType = "com.apple.product-type.application"; 82 | }; 83 | /* End PBXNativeTarget section */ 84 | 85 | /* Begin PBXProject section */ 86 | 799EB4D51BA42D4D00E8F920 /* Project object */ = { 87 | isa = PBXProject; 88 | attributes = { 89 | LastSwiftUpdateCheck = 0700; 90 | LastUpgradeCheck = 1220; 91 | ORGANIZATIONNAME = "Ce Zheng"; 92 | TargetAttributes = { 93 | 799EB4DC1BA42D4D00E8F920 = { 94 | CreatedOnToolsVersion = 7.0; 95 | LastSwiftMigration = 1020; 96 | }; 97 | }; 98 | }; 99 | buildConfigurationList = 799EB4D81BA42D4D00E8F920 /* Build configuration list for PBXProject "FuziDemo" */; 100 | compatibilityVersion = "Xcode 3.2"; 101 | developmentRegion = en; 102 | hasScannedForEncodings = 0; 103 | knownRegions = ( 104 | en, 105 | Base, 106 | ); 107 | mainGroup = 799EB4D41BA42D4D00E8F920; 108 | productRefGroup = 799EB4DE1BA42D4D00E8F920 /* Products */; 109 | projectDirPath = ""; 110 | projectRoot = ""; 111 | targets = ( 112 | 799EB4DC1BA42D4D00E8F920 /* FuziDemo */, 113 | ); 114 | }; 115 | /* End PBXProject section */ 116 | 117 | /* Begin PBXResourcesBuildPhase section */ 118 | 799EB4DB1BA42D4D00E8F920 /* Resources */ = { 119 | isa = PBXResourcesBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | 7932B15E1BA7FAD4008CA6F5 /* nutrition.xml in Resources */, 123 | ); 124 | runOnlyForDeploymentPostprocessing = 0; 125 | }; 126 | /* End PBXResourcesBuildPhase section */ 127 | 128 | /* Begin PBXSourcesBuildPhase section */ 129 | 799EB4D91BA42D4D00E8F920 /* Sources */ = { 130 | isa = PBXSourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | 7932B1151BA7EC67008CA6F5 /* main.swift in Sources */, 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXSourcesBuildPhase section */ 138 | 139 | /* Begin XCBuildConfiguration section */ 140 | 799EB4E81BA42D4D00E8F920 /* Debug */ = { 141 | isa = XCBuildConfiguration; 142 | buildSettings = { 143 | ALWAYS_SEARCH_USER_PATHS = NO; 144 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 145 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 146 | CLANG_CXX_LIBRARY = "libc++"; 147 | CLANG_ENABLE_MODULES = YES; 148 | CLANG_ENABLE_OBJC_ARC = YES; 149 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 150 | CLANG_WARN_BOOL_CONVERSION = YES; 151 | CLANG_WARN_COMMA = YES; 152 | CLANG_WARN_CONSTANT_CONVERSION = YES; 153 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 154 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 155 | CLANG_WARN_EMPTY_BODY = YES; 156 | CLANG_WARN_ENUM_CONVERSION = YES; 157 | CLANG_WARN_INFINITE_RECURSION = YES; 158 | CLANG_WARN_INT_CONVERSION = YES; 159 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 160 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 161 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 162 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 163 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 164 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 165 | CLANG_WARN_STRICT_PROTOTYPES = YES; 166 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 167 | CLANG_WARN_UNREACHABLE_CODE = YES; 168 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 169 | CODE_SIGN_IDENTITY = "-"; 170 | COPY_PHASE_STRIP = NO; 171 | DEBUG_INFORMATION_FORMAT = dwarf; 172 | ENABLE_STRICT_OBJC_MSGSEND = YES; 173 | ENABLE_TESTABILITY = YES; 174 | GCC_C_LANGUAGE_STANDARD = gnu99; 175 | GCC_DYNAMIC_NO_PIC = NO; 176 | GCC_NO_COMMON_BLOCKS = YES; 177 | GCC_OPTIMIZATION_LEVEL = 0; 178 | GCC_PREPROCESSOR_DEFINITIONS = ( 179 | "DEBUG=1", 180 | "$(inherited)", 181 | ); 182 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 183 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 184 | GCC_WARN_UNDECLARED_SELECTOR = YES; 185 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 186 | GCC_WARN_UNUSED_FUNCTION = YES; 187 | GCC_WARN_UNUSED_VARIABLE = YES; 188 | MACOSX_DEPLOYMENT_TARGET = 10.10; 189 | MTL_ENABLE_DEBUG_INFO = YES; 190 | ONLY_ACTIVE_ARCH = YES; 191 | SDKROOT = macosx; 192 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 193 | }; 194 | name = Debug; 195 | }; 196 | 799EB4E91BA42D4D00E8F920 /* Release */ = { 197 | isa = XCBuildConfiguration; 198 | buildSettings = { 199 | ALWAYS_SEARCH_USER_PATHS = NO; 200 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 202 | CLANG_CXX_LIBRARY = "libc++"; 203 | CLANG_ENABLE_MODULES = YES; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 206 | CLANG_WARN_BOOL_CONVERSION = YES; 207 | CLANG_WARN_COMMA = YES; 208 | CLANG_WARN_CONSTANT_CONVERSION = YES; 209 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 210 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 221 | CLANG_WARN_STRICT_PROTOTYPES = YES; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNREACHABLE_CODE = YES; 224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 | CODE_SIGN_IDENTITY = "-"; 226 | COPY_PHASE_STRIP = NO; 227 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 228 | ENABLE_NS_ASSERTIONS = NO; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | GCC_C_LANGUAGE_STANDARD = gnu99; 231 | GCC_NO_COMMON_BLOCKS = YES; 232 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 233 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 234 | GCC_WARN_UNDECLARED_SELECTOR = YES; 235 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 236 | GCC_WARN_UNUSED_FUNCTION = YES; 237 | GCC_WARN_UNUSED_VARIABLE = YES; 238 | MACOSX_DEPLOYMENT_TARGET = 10.10; 239 | MTL_ENABLE_DEBUG_INFO = NO; 240 | SDKROOT = macosx; 241 | }; 242 | name = Release; 243 | }; 244 | 799EB4EB1BA42D4D00E8F920 /* Debug */ = { 245 | isa = XCBuildConfiguration; 246 | buildSettings = { 247 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 248 | CLANG_ENABLE_MODULES = YES; 249 | CODE_SIGN_IDENTITY = "-"; 250 | COMBINE_HIDPI_IMAGES = YES; 251 | HEADER_SEARCH_PATHS = ( 252 | "$(inherited)", 253 | "$(SDKROOT)/usr/include/libxml2", 254 | ); 255 | INFOPLIST_FILE = FuziDemo/Info.plist; 256 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 257 | OTHER_LDFLAGS = "$(inherited)"; 258 | PRODUCT_BUNDLE_IDENTIFIER = me.cezheng.FuzeDemo; 259 | PRODUCT_NAME = FuziDemo; 260 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 261 | SWIFT_VERSION = 5.0; 262 | }; 263 | name = Debug; 264 | }; 265 | 799EB4EC1BA42D4D00E8F920 /* Release */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 269 | CLANG_ENABLE_MODULES = YES; 270 | CODE_SIGN_IDENTITY = "-"; 271 | COMBINE_HIDPI_IMAGES = YES; 272 | HEADER_SEARCH_PATHS = ( 273 | "$(inherited)", 274 | "$(SDKROOT)/usr/include/libxml2", 275 | ); 276 | INFOPLIST_FILE = FuziDemo/Info.plist; 277 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 278 | OTHER_LDFLAGS = "$(inherited)"; 279 | PRODUCT_BUNDLE_IDENTIFIER = me.cezheng.FuzeDemo; 280 | PRODUCT_NAME = FuziDemo; 281 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 282 | SWIFT_VERSION = 5.0; 283 | }; 284 | name = Release; 285 | }; 286 | /* End XCBuildConfiguration section */ 287 | 288 | /* Begin XCConfigurationList section */ 289 | 799EB4D81BA42D4D00E8F920 /* Build configuration list for PBXProject "FuziDemo" */ = { 290 | isa = XCConfigurationList; 291 | buildConfigurations = ( 292 | 799EB4E81BA42D4D00E8F920 /* Debug */, 293 | 799EB4E91BA42D4D00E8F920 /* Release */, 294 | ); 295 | defaultConfigurationIsVisible = 0; 296 | defaultConfigurationName = Release; 297 | }; 298 | 799EB4EA1BA42D4D00E8F920 /* Build configuration list for PBXNativeTarget "FuziDemo" */ = { 299 | isa = XCConfigurationList; 300 | buildConfigurations = ( 301 | 799EB4EB1BA42D4D00E8F920 /* Debug */, 302 | 799EB4EC1BA42D4D00E8F920 /* Release */, 303 | ); 304 | defaultConfigurationIsVisible = 0; 305 | defaultConfigurationName = Release; 306 | }; 307 | /* End XCConfigurationList section */ 308 | }; 309 | rootObject = 799EB4D51BA42D4D00E8F920 /* Project object */; 310 | } 311 | -------------------------------------------------------------------------------- /FuziDemo/FuziDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FuziDemo/FuziDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2015 Ce Zheng. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /FuziDemo/FuziDemo/main.swift: -------------------------------------------------------------------------------- 1 | // main.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | // This example does exactly the same thing as Ono's example 23 | // https://github.com/mattt/Ono/blob/master/Example/main.m 24 | // Comparing the two may help migrating your code from Ono 25 | 26 | import Fuzi 27 | 28 | let fileUrl = URL(fileURLWithPath: ((#file as NSString).deletingLastPathComponent as NSString).appendingPathComponent("nutrition.xml")) 29 | do { 30 | let data = try Data(contentsOf: fileUrl) 31 | let document = try XMLDocument(data: data) 32 | 33 | if let root = document.root { 34 | print("Root Element: \(String(describing: root.tag))") 35 | 36 | print("\nDaily values:") 37 | for element in root.firstChild(tag: "daily-values")?.children ?? [] { 38 | let nutrient = element.tag 39 | let amount = element.numberValue 40 | let unit = element["units"] 41 | print("- \(amount!)\(unit!) \(nutrient!)") 42 | } 43 | print("\n") 44 | var xpath = "//food/name" 45 | print("XPath Search: \(xpath)") 46 | for element in document.xpath(xpath) { 47 | print("\(element)") 48 | } 49 | 50 | print("\n") 51 | let css = "food > serving[units]" 52 | var blockElement:XMLElement? = nil 53 | print("CSS Search: \(css)") 54 | for (index, element) in document.css(css).enumerated() { 55 | if index == 1 { 56 | blockElement = element 57 | break 58 | } 59 | } 60 | print("Second element: \(blockElement!)\n") 61 | 62 | xpath = "//food/name" 63 | print("XPath Search: \(xpath)") 64 | let firstElement = document.firstChild(xpath: xpath)! 65 | print("First element: \(firstElement)") 66 | } 67 | } catch let error as XMLError { 68 | switch error { 69 | case .noError: print("wth this should not appear") 70 | case .parserFailure, .invalidData, .xpathError: print(error) 71 | case .libXMLError(let code, let message): 72 | print("libxml error code: \(code), message: \(message)") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /FuziDemo/FuziDemo/nutrition.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 65 5 | 20 6 | 300 7 | 2400 8 | 300 9 | 25 10 | 50 11 | 12 | 13 | Avocado Dip 14 | Sunnydale 15 | 29 16 | 17 | 11 18 | 3 19 | 5 20 | 210 21 | 2 22 | 0 23 | 1 24 | 25 | 0 26 | 0 27 | 28 | 29 | 0 30 | 0 31 | 32 | 33 | 34 | Bagels, New York Style 35 | Thompson 36 | 104 37 | 38 | 4 39 | 1 40 | 0 41 | 510 42 | 54 43 | 3 44 | 11 45 | 46 | 0 47 | 0 48 | 49 | 50 | 8 51 | 20 52 | 53 | 54 | 55 | Beef Frankfurter, Quarter Pound 56 | Armitage 57 | 115 58 | 59 | 32 60 | 15 61 | 65 62 | 1100 63 | 8 64 | 0 65 | 13 66 | 67 | 0 68 | 2 69 | 70 | 71 | 1 72 | 6 73 | 74 | 75 | 76 | Chicken Pot Pie 77 | Lakeson 78 | 198 79 | 80 | 22 81 | 9 82 | 25 83 | 810 84 | 42 85 | 2 86 | 10 87 | 88 | 20 89 | 2 90 | 91 | 92 | 2 93 | 10 94 | 95 | 96 | 97 | Cole Slaw 98 | Fresh Quick 99 | 1.5 100 | 101 | 0 102 | 0 103 | 0 104 | 15 105 | 5 106 | 2 107 | 1 108 | 109 | 30 110 | 45 111 | 112 | 113 | 4 114 | 2 115 | 116 | 117 | 118 | Eggs 119 | Goodpath 120 | 50 121 | 122 | 4.5 123 | 1.5 124 | 215 125 | 65 126 | 1 127 | 0 128 | 6 129 | 130 | 6 131 | 0 132 | 133 | 134 | 2 135 | 4 136 | 137 | 138 | 139 | Hazelnut Spread 140 | Ferreira 141 | 2 142 | 143 | 10 144 | 2 145 | 0 146 | 20 147 | 23 148 | 2 149 | 3 150 | 151 | 0 152 | 0 153 | 154 | 155 | 6 156 | 4 157 | 158 | 159 | 160 | Potato Chips 161 | Lees 162 | 28 163 | 164 | 10 165 | 3 166 | 0 167 | 180 168 | 15 169 | 1 170 | 2 171 | 172 | 0 173 | 10 174 | 175 | 176 | 0 177 | 0 178 | 179 | 180 | 181 | Soy Patties, Grilled 182 | Gardenproducts 183 | 96 184 | 185 | 5 186 | 0 187 | 0 188 | 420 189 | 10 190 | 4 191 | 9 192 | 193 | 0 194 | 0 195 | 196 | 197 | 0 198 | 0 199 | 200 | 201 | 202 | Truffles, Dark Chocolate 203 | Lyndon's 204 | 39 205 | 206 | 19 207 | 14 208 | 25 209 | 10 210 | 16 211 | 1 212 | 1 213 | 214 | 0 215 | 0 216 | 217 | 218 | 0 219 | 0 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ce Zheng 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | 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 OR 14 | 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 FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Fuzi", 8 | products: [ 9 | .library(name: "Fuzi", targets: ["Fuzi"]), 10 | ], 11 | targets: [ 12 | .target(name: "Fuzi", 13 | path: "Sources", 14 | linkerSettings: [.linkedLibrary("xml2")] 15 | ), 16 | .testTarget(name: "FuziTests", 17 | dependencies: ["Fuzi"], 18 | path: "Tests" 19 | ) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README-ja.md: -------------------------------------------------------------------------------- 1 | # Fuzi (斧子) 2 | 3 | [![Build Status](https://api.travis-ci.org/cezheng/Fuzi.svg)](https://travis-ci.org/cezheng/Fuzi) 4 | [![Cocoapods Compatible](https://img.shields.io/cocoapods/v/Fuzi.svg)](https://cocoapods.org/pods/Fuzi) 5 | [![License](https://img.shields.io/cocoapods/l/Fuzi.svg?style=flat&color=gray)](http://opensource.org/licenses/MIT) 6 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![Platform](https://img.shields.io/cocoapods/p/Fuzi.svg?style=flat)](http://cezheng.github.io/Fuzi/) 8 | [![Twitter](https://img.shields.io/badge/twitter-@AdamoCheng-blue.svg?style=flat)](http://twitter.com/AdamoCheng) 9 | 10 | **軽くて、素早くて、 Swift の XML/HTML パーサー。** [[ドキュメント]](http://cezheng.github.io/Fuzi/) 11 | 12 | Fuzi は Mattt Thompson氏の [Ono](https://github.com/mattt/Ono)(斧) に参照し Swift 言語で実装した XML/HTML パーサーである。 13 | 14 | > Fuzi は漢字の`斧子`の中国語発音で、 意味は[Ono](https://github.com/mattt/Ono)(斧)と同じ。Onoは、[Nokogiri](http://nokogiri.org)(鋸)を参照し、創ったもの。 15 | 16 | [English](https://github.com/cezheng/Fuzi/blob/master/README.md) 17 | [简体中文](https://github.com/cezheng/Fuzi/blob/master/README-zh.md) 18 | ## クイックルック 19 | ```swift 20 | let xml = "..." 21 | // or 22 | // let xmlData = 23 | do { 24 | let document = try XMLDocument(string: xml) 25 | // or 26 | // let document = try XMLDocument(data: xmlData) 27 | 28 | if let root = document.root { 29 | // Accessing all child nodes of root element 30 | for element in root.children { 31 | print("\(element.tag): \(element.attributes)") 32 | } 33 | 34 | // Getting child element by tag & accessing attributes 35 | if let length = root.firstChild(tag:"Length", inNamespace: "dc") { 36 | print(length["unit"]) // `unit` attribute 37 | print(length.attributes) // all attributes 38 | } 39 | } 40 | 41 | // XPath & CSS queries 42 | for element in document.xpath("//element") { 43 | print("\(element.tag): \(element.attributes)") 44 | } 45 | 46 | if let firstLink = document.firstChild(css: "a, link") { 47 | print(firstLink["href"]) 48 | } 49 | } catch let error { 50 | print(error) 51 | } 52 | ``` 53 | 54 | ## 機能 55 | ### Onoから貰った機能 56 | - `libxml2`での素早いXMLパース 57 | - [XPath](http://en.wikipedia.org/wiki/XPath) と [CSS](http://en.wikipedia.org/wiki/Cascading_Style_Sheets) クエリ 58 | - 自動的にデータを日付や数字に変換する 59 | - XML ネイムスペース 60 | - `String` や `NSData` や `[CChar]`からXMLDocumentをロードする 61 | - 全面的なユニットテスト 62 | - 100%ドキュメント 63 | 64 | ### Fuziの改善点 65 | - Swift 言語のネーミングやコーディングルールに沿って、クラスやメソッドを再設計した 66 | - 日付や数字変換のフォマットを指定できる 67 | - いくつかのバグ修正 68 | - より多くのHTML便利メソッド 69 | - 全種類のXMLノード取得可能(テキストノードやコメントノードなども含め) 70 | - より多くのCSSクエリ対応 (これから) 71 | 72 | 73 | 74 | ## 環境 75 | 76 | - iOS 8.0+ / Mac OS X 10.9+ 77 | - Xcode 8.0+ 78 | 79 | > Swift 2.3は[0.4.0](../../releases/tag/0.4.0)をご利用ください。 80 | 81 | ## インストール 82 | ### CocoaPodsで 83 | [Cocoapods](http://cocoapods.org/) で簡単に `Fuzi` をインストールできます。 下記のように`Podfile`を編集してください: 84 | 85 | ```ruby 86 | platform :ios, '8.0' 87 | use_frameworks! 88 | 89 | target 'MyApp' do 90 | pod 'Fuzi', '~> 1.0.0' 91 | end 92 | ``` 93 | 94 | そして、下記のコマンドを実行してください: 95 | 96 | ```bash 97 | $ pod install 98 | ``` 99 | 100 | ### 手動で 101 | 1. `Fuzi`フォルダの `*.swift` ファイルをプロジェクトに追加してください。 102 | 2. Xcode プロジェクトの `Build Settings` で: 103 | 1. `Search Paths`の`Header Search Paths`に`$(SDKROOT)/usr/include/libxml2`を追加してください。 104 | 2. `Linking`の`Other Linker Flags`に`-lxml2`を追加してください。 105 | 106 | ### Carthageで 107 | プロダクトのディレクトリに`Cartfile` か `Cartfile.private`のファイルを作成し、下記の行を追加してください: 108 | 109 | ``` 110 | github "cezheng/Fuzi" ~> 1.0.0 111 | ``` 112 | そして、下記のコマンドを実行してください: 113 | 114 | ``` 115 | $ carthage update 116 | ``` 117 | 最後に、下記のようにXcodeのtargetを設定してください: 118 | 119 | 1. ビルドターゲットの`General` -> `Embedded Binaries`に、Carthageがビルドした`Fuzi.framework`を追加してください。 120 | 2. `Build Settings`で`Search Paths`の`Header Search Paths`に`$(SDKROOT)/usr/include/libxml2`を追加してください。 121 | 122 | 123 | ## 用例 124 | ### XML 125 | ```swift 126 | import Fuzi 127 | 128 | let xml = "..." 129 | do { 130 | // if encoding is omitted, it defaults to NSUTF8StringEncoding 131 | let document = try XMLDocument(string: html, encoding: NSUTF8StringEncoding) 132 | if let root = document.root { 133 | print(root.tag) 134 | 135 | // define a prefix for a namespace 136 | document.definePrefix("atom", defaultNamespace: "http://www.w3.org/2005/Atom") 137 | 138 | // get first child element with given tag in namespace(optional) 139 | print(root.firstChild(tag: "title", inNamespace: "atom")) 140 | 141 | // iterate through all children 142 | for element in root.children { 143 | print("\(index) \(element.tag): \(element.attributes)") 144 | } 145 | } 146 | // you can also use CSS selector against XMLDocument when you feels it makes sense 147 | } catch let error as XMLError { 148 | switch error { 149 | case .noError: print("wth this should not appear") 150 | case .parserFailure, .invalidData: print(error) 151 | case .libXMLError(let code, let message): 152 | print("libxml error code: \(code), message: \(message)") 153 | } 154 | } 155 | ``` 156 | ### HTML 157 | `HTMLDocument` は `XMLDocument` サブクラス。 158 | 159 | ```swift 160 | import Fuzi 161 | 162 | let html = "..." 163 | do { 164 | // if encoding is omitted, it defaults to NSUTF8StringEncoding 165 | let doc = try HTMLDocument(string: html, encoding: NSUTF8StringEncoding) 166 | 167 | // CSS queries 168 | if let elementById = doc.firstChild(css: "#id") { 169 | print(elementById.stringValue) 170 | } 171 | for link in doc.css("a, link") { 172 | print(link.rawXML) 173 | print(link["href"]) 174 | } 175 | 176 | // XPath queries 177 | if let firstAnchor = doc.firstChild(xpath: "//body/a") { 178 | print(firstAnchor["href"]) 179 | } 180 | for script in doc.xpath("//head/script") { 181 | print(script["src"]) 182 | } 183 | 184 | // Evaluate XPath functions 185 | if let result = doc.eval(xpath: "count(/*/a)") { 186 | print("anchor count : \(result.doubleValue)") 187 | } 188 | 189 | // Convenient HTML methods 190 | print(doc.title) // gets 's innerHTML in <head> 191 | print(doc.head) // gets <head> element 192 | print(doc.body) // gets <body> element 193 | 194 | } catch let error { 195 | print(error) 196 | } 197 | ``` 198 | 199 | ### エラー処理なんて、どうでもいい場合 200 | 201 | ```swift 202 | import Fuzi 203 | 204 | let xml = "..." 205 | 206 | // Don't show me the errors, just don't crash 207 | if let doc1 = try? XMLDocument(string: xml) { 208 | //... 209 | } 210 | 211 | let html = "<html>...</html>" 212 | 213 | // I'm sure this won't crash 214 | let doc2 = try! HTMLDocument(string: html) 215 | //... 216 | ``` 217 | 218 | ### テキストノードを取得したい 219 | テキストノードだけではなく、全種類のノードは取得可能。 220 | 221 | ```swift 222 | let document = ... 223 | // すべてのエレメント、テキストとコメント子要素を取得する 224 | document.root?.childNodes(ofTypes: [.Element, .Text, .Comment]) 225 | 226 | ##Onoからの移行? 227 | 下記2つのサンプルコードを見たら、`Ono`と`Fuzi`の違いをわかる。 228 | 229 | [Onoサンプル](https://github.com/mattt/Ono/blob/master/Example/main.m) 230 | 231 | [Fuziサンプル](FuziDemo/FuziDemo/main.swift) 232 | 233 | ###子要素を取得 234 | **Ono** 235 | 236 | ```objc 237 | [doc firstChildWithTag:tag inNamespace:namespace]; 238 | [doc firstChildWithXPath:xpath]; 239 | [doc firstChildWithXPath:css]; 240 | for (ONOXMLElement *element in parent.children) { 241 | //... 242 | } 243 | [doc childrenWithTag:tag inNamespace:namespace]; 244 | ``` 245 | **Fuzi** 246 | 247 | ```swift 248 | doc.firstChild(tag: tag, inNamespace: namespace) 249 | doc.firstChild(xpath: xpath) 250 | doc.firstChild(css: css) 251 | for element in parent.children { 252 | //... 253 | } 254 | doc.children(tag: tag, inNamespace:namespace) 255 | ``` 256 | ###クエリ結果を読み込む 257 | **Ono** 258 | 259 | Objective-Cの`NSFastEnumeration`。 260 | 261 | ```objc 262 | // simply iterating through the results 263 | // mark `__unused` to unused params `idx` and `stop` 264 | [doc enumerateElementsWithXPath:xpath usingBlock:^(ONOXMLElement *element, __unused NSUInteger idx, __unused BOOL *stop) { 265 | NSLog(@"%@", element); 266 | }]; 267 | 268 | // stop the iteration at second element 269 | [doc enumerateElementsWithXPath:XPath usingBlock:^(ONOXMLElement *element, NSUInteger idx, BOOL *stop) { 270 | *stop = (idx == 1); 271 | }]; 272 | 273 | // getting element by index 274 | ONOXMLDocument *nthElement = [(NSEnumerator*)[doc CSS:css] allObjects][n]; 275 | 276 | // total element count 277 | NSUInteger count = [(NSEnumerator*)[document XPath:xpath] allObjects].count; 278 | ``` 279 | 280 | **Fuzi** 281 | 282 | Swift の `SequenceType` と `Indexable`。 283 | 284 | ```swift 285 | // simply iterating through the results 286 | // no need to write the unused `idx` or `stop` params 287 | for element in doc.xpath(xpath) { 288 | print(element) 289 | } 290 | 291 | // stop the iteration at second element 292 | for (index, element) in doc.xpath(xpath).enumerate() { 293 | if idx == 1 { 294 | break 295 | } 296 | } 297 | 298 | // getting element by index 299 | if let nthElement = doc.css(css)[n] { 300 | //... 301 | } 302 | 303 | // total element count 304 | let count = doc.xpath(xpath).count 305 | ``` 306 | 307 | ###XPath関数を評価する 308 | **Ono** 309 | 310 | ```objc 311 | ONOXPathFunctionResult *result = [doc functionResultByEvaluatingXPath:xpath]; 312 | result.boolValue; //BOOL 313 | result.numericValue; //double 314 | result.stringValue; //NSString 315 | ``` 316 | 317 | **Fuzi** 318 | 319 | ```swift 320 | if let result = doc.eval(xpath: xpath) { 321 | result.boolValue //Bool 322 | result.doubleValue //Double 323 | result.stringValue //String 324 | } 325 | ``` 326 | ## ライセンス 327 | 328 | `Fuzi` のオープンソースライセンスは MIT です。 詳しくはこちら [LICENSE](LICENSE) 。 329 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # Fuzi (斧子) 2 | 3 | [![Build Status](https://api.travis-ci.org/cezheng/Fuzi.svg)](https://travis-ci.org/cezheng/Fuzi) 4 | [![Cocoapods Compatible](https://img.shields.io/cocoapods/v/Fuzi.svg)](https://cocoapods.org/pods/Fuzi) 5 | [![License](https://img.shields.io/cocoapods/l/Fuzi.svg?style=flat&color=gray)](http://opensource.org/licenses/MIT) 6 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![Platform](https://img.shields.io/cocoapods/p/Fuzi.svg?style=flat)](http://cezheng.github.io/Fuzi/) 8 | [![Twitter](https://img.shields.io/badge/twitter-@AdamoCheng-blue.svg?style=flat)](http://twitter.com/AdamoCheng) 9 | [![Weibo](https://img.shields.io/badge/weibo-Real__Adam-red.svg)](http://weibo.com/cezheng) 10 | 11 | **Swift实现的轻量快速的 XML/HTML 解析器。** [[文档]](http://cezheng.github.io/Fuzi/) 12 | 13 | Mattt Thompson大神的 [Ono](https://github.com/mattt/Ono)(斧) 是iOS/OSX平台上非常好用的一个XML/HTML 解析库。用ObjectiveC实现的Ono在Swift的应用里虽然可以使用,却有诸多不便。因此鄙人参照了Ono对libxml2的封装方式,对类和方法进行了重新设计弄出了这个小库。同时修正了Ono存在的一些逻辑上和内存管理方面的bug。 14 | 15 | > Fuzi(斧子) 大家都懂是啥意思,[Ono](https://github.com/mattt/Ono)(斧)则是`斧`这个汉字的日语读法, 因为Mattt神写出Ono是受了 [Nokogiri](http://nokogiri.org) (鋸)的启发,取了一个同类的名词向其致敬。 16 | 17 | [English](README.md) 18 | [日本語](README-ja.md) 19 | 20 | ## 一个简单的例子 21 | ```swift 22 | let xml = "..." 23 | // or 24 | // let xmlData = <some NSData or Data> 25 | do { 26 | let document = try XMLDocument(string: xml) 27 | // or 28 | // let document = try XMLDocument(data: xmlData) 29 | 30 | if let root = document.root { 31 | // Accessing all child nodes of root element 32 | for element in root.children { 33 | print("\(element.tag): \(element.attributes)") 34 | } 35 | 36 | // Getting child element by tag & accessing attributes 37 | if let length = root.firstChild(tag:"Length", inNamespace: "dc") { 38 | print(length["unit"]) // `unit` attribute 39 | print(length.attributes) // all attributes 40 | } 41 | } 42 | 43 | // XPath & CSS queries 44 | for element in document.xpath("//element") { 45 | print("\(element.tag): \(element.attributes)") 46 | } 47 | 48 | if let firstLink = document.firstChild(css: "a, link") { 49 | print(firstLink["href"]) 50 | } 51 | } catch let error { 52 | print(error) 53 | } 54 | ``` 55 | 56 | ## 特性 57 | ### 继承自Ono 58 | - 借助`libxml2`实现的快速解析 59 | - [XPath](http://en.wikipedia.org/wiki/XPath) 和 [CSS](http://en.wikipedia.org/wiki/Cascading_Style_Sheets) 查询 60 | - 日期和数字的自动转换 61 | - 支持XML命名空间 62 | - 能从`String`,`NSData` 或 `[CChar]`构建XML文档 63 | - 全面的自动测试 64 | - 文档覆盖率100% 65 | 66 | ### Fuzi的改进 67 | - 遵循Swift规范的命名和API重新设计,避免了在Swift中使用Ono的很多不便 68 | - 可以规定自动转换日期和数字的格式了 69 | - 修正了一些bug 70 | - 增加更多常用的HTML处理方法 71 | - 支持获取所有类型的节点(包括文字节点,注释节点等) 72 | - 支持更多的CSS查询类型 (今后将会支持) 73 | 74 | ## 环境 75 | 76 | - iOS 8.0+ / Mac OS X 10.9+ 77 | - Xcode 8.0+ 78 | 79 | > Swift 2.3 请使用[0.4.0](../../releases/tag/0.4.0)版。 80 | 81 | 82 | ## 导入 83 | ### 通过[Cocoapods](http://cocoapods.org/) 84 | 您可以通过 [Cocoapods](http://cocoapods.org/) 来将 `Fuzi` 添加到您的项目中。 下面是一个示例的`Podfile`: 85 | 86 | ```ruby 87 | platform :ios, '8.0' 88 | use_frameworks! 89 | 90 | target 'MyApp' do 91 | pod 'Fuzi', '~> 1.0.0' 92 | end 93 | ``` 94 | 95 | 配置好Podfile后执行如下命令: 96 | 97 | ```bash 98 | $ pod install 99 | ``` 100 | 101 | ### 手动导入 102 | 1. 将`Fuzi`文件夹下所有`*.swift`文件添加到您的Xcode项目中。 103 | 2. 修改Xcode项目的`Build Settings`: 104 | 1. 向`Search Paths`的`Header Search Paths`条目下添加`$(SDKROOT)/usr/include/libxml2`。 105 | 2. 向`Linking`的`Other Linker Flags`条目下添加`-lxml2`。 106 | 107 | ### 通过[Carthage](https://github.com/Carthage/Carthage) 108 | 在项目的根目录下创建名为 `Cartfile` 或 `Cartfile.private`的文件,并加入如下一行: 109 | 110 | ``` 111 | github "cezheng/Fuzi" ~> 1.0.0 112 | ``` 113 | 然后执行如下命令: 114 | 115 | ``` 116 | $ carthage update 117 | ``` 118 | 最后对Xcode的目标做如下设置: 119 | 120 | 1. 将Carthage编译出来的`Fuzi.framework`拖拽如目标的`General` -> `Embedded Binaries`。 121 | 2. `Build Settings`中,向`Search Paths`的`Header Search Paths`条目下添加`$(SDKROOT)/usr/include/libxml2`。 122 | 123 | 124 | ## 例子 125 | ### XML 126 | ```swift 127 | import Fuzi 128 | 129 | let xml = "..." 130 | do { 131 | // if encoding is omitted, it defaults to NSUTF8StringEncoding 132 | let document = try XMLDocument(string: html, encoding: NSUTF8StringEncoding) 133 | if let root = document.root { 134 | print(root.tag) 135 | 136 | // define a prefix for a namespace 137 | document.definePrefix("atom", defaultNamespace: "http://www.w3.org/2005/Atom") 138 | 139 | // get first child element with given tag in namespace(optional) 140 | print(root.firstChild(tag: "title", inNamespace: "atom")) 141 | 142 | // iterate through all children 143 | for element in root.children { 144 | print("\(index) \(element.tag): \(element.attributes)") 145 | } 146 | } 147 | // you can also use CSS selector against XMLDocument when you feels it makes sense 148 | } catch let error as XMLError { 149 | switch error { 150 | case .noError: print("wth this should not appear") 151 | case .parserFailure, .invalidData: print(error) 152 | case .libXMLError(let code, let message): 153 | print("libxml error code: \(code), message: \(message)") 154 | } 155 | } 156 | ``` 157 | ### HTML 158 | `HTMLDocument` 是 `XMLDocument` 的子类。 159 | 160 | ```swift 161 | import Fuzi 162 | 163 | let html = "<html>...</html>" 164 | do { 165 | // if encoding is omitted, it defaults to NSUTF8StringEncoding 166 | let doc = try HTMLDocument(string: html, encoding: NSUTF8StringEncoding) 167 | 168 | // CSS queries 169 | if let elementById = doc.firstChild(css: "#id") { 170 | print(elementById.stringValue) 171 | } 172 | for link in doc.css("a, link") { 173 | print(link.rawXML) 174 | print(link["href"]) 175 | } 176 | 177 | // XPath queries 178 | if let firstAnchor = doc.firstChild(xpath: "//body/a") { 179 | print(firstAnchor["href"]) 180 | } 181 | for script in doc.xpath("//head/script") { 182 | print(script["src"]) 183 | } 184 | 185 | // Evaluate XPath functions 186 | if let result = doc.eval(xpath: "count(/*/a)") { 187 | print("anchor count : \(result.doubleValue)") 188 | } 189 | 190 | // Convenient HTML methods 191 | print(doc.title) // gets <title>'s innerHTML in <head> 192 | print(doc.head) // gets <head> element 193 | print(doc.body) // gets <body> element 194 | 195 | } catch let error { 196 | print(error) 197 | } 198 | ``` 199 | 200 | ### 如果觉得没必要处理异常 201 | 202 | ```swift 203 | import Fuzi 204 | 205 | let xml = "..." 206 | 207 | // Don't show me the errors, just don't crash 208 | if let doc1 = try? XMLDocument(string: xml) { 209 | //... 210 | } 211 | 212 | let html = "<html>...</html>" 213 | 214 | // I'm sure this won't crash 215 | let doc2 = try! HTMLDocument(string: html) 216 | //... 217 | ``` 218 | 219 | ### 我想访问文字节点 220 | 不仅文字节点,你可以指定你想获取的任何类型的节点。 221 | 222 | ```swift 223 | let document = ... 224 | // 获取所有类型为元素,文字或注释的节点 225 | document.root?.childNodes(ofTypes: [.Element, .Text, .Comment]) 226 | ``` 227 | 228 | ## 从Ono转移到Fuzi 229 | 下面两个示例程序做的事情是完全一样的,通过比较能很快了解两者的异同。 230 | 231 | [Ono示例](https://github.com/mattt/Ono/blob/master/Example/main.m) 232 | 233 | [Fuzi示例](FuziDemo/FuziDemo/main.swift) 234 | 235 | ### 访问子节点 236 | **Ono** 237 | 238 | ```objc 239 | [doc firstChildWithTag:tag inNamespace:namespace]; 240 | [doc firstChildWithXPath:xpath]; 241 | [doc firstChildWithXPath:css]; 242 | for (ONOXMLElement *element in parent.children) { 243 | //... 244 | } 245 | [doc childrenWithTag:tag inNamespace:namespace]; 246 | ``` 247 | **Fuzi** 248 | 249 | ```swift 250 | doc.firstChild(tag: tag, inNamespace: namespace) 251 | doc.firstChild(xpath: xpath) 252 | doc.firstChild(css: css) 253 | for element in parent.children { 254 | //... 255 | } 256 | doc.children(tag: tag, inNamespace:namespace) 257 | ``` 258 | ### 迭代查询结果 259 | **Ono** 260 | 261 | 查询结果实现了`NSFastEnumeration`协议。 262 | 263 | ```objc 264 | // simply iterating through the results 265 | // mark `__unused` to unused params `idx` and `stop` 266 | [doc enumerateElementsWithXPath:xpath usingBlock:^(ONOXMLElement *element, __unused NSUInteger idx, __unused BOOL *stop) { 267 | NSLog(@"%@", element); 268 | }]; 269 | 270 | // stop the iteration at second element 271 | [doc enumerateElementsWithXPath:XPath usingBlock:^(ONOXMLElement *element, NSUInteger idx, BOOL *stop) { 272 | *stop = (idx == 1); 273 | }]; 274 | 275 | // getting element by index 276 | ONOXMLDocument *nthElement = [(NSEnumerator*)[doc CSS:css] allObjects][n]; 277 | 278 | // total element count 279 | NSUInteger count = [(NSEnumerator*)[document XPath:xpath] allObjects].count; 280 | ``` 281 | 282 | **Fuzi** 283 | 284 | 查询结果的集合实现了Swift的 `SequenceType` 与 `Indexable`协议。 285 | 286 | ```swift 287 | // simply iterating through the results 288 | // no need to write the unused `idx` or `stop` params 289 | for element in doc.xpath(xpath) { 290 | print(element) 291 | } 292 | 293 | // stop the iteration at second element 294 | for (index, element) in doc.xpath(xpath).enumerate() { 295 | if idx == 1 { 296 | break 297 | } 298 | } 299 | 300 | // getting element by index 301 | if let nthElement = doc.css(css)[n] { 302 | //... 303 | } 304 | 305 | // total element count 306 | let count = doc.xpath(xpath).count 307 | ``` 308 | ### 执行XPath函数 309 | **Ono** 310 | 311 | ```objc 312 | ONOXPathFunctionResult *result = [doc functionResultByEvaluatingXPath:xpath]; 313 | result.boolValue; //BOOL 314 | result.numericValue; //double 315 | result.stringValue; //NSString 316 | ``` 317 | 318 | **Fuzi** 319 | 320 | ```swift 321 | if let result = doc.eval(xpath: xpath) { 322 | result.boolValue //Bool 323 | result.doubleValue //Double 324 | result.stringValue //String 325 | } 326 | ``` 327 | 328 | ## 开源协议 329 | 330 | `Fuzi` 使用MIT许可协议。详见 [LICENSE](LICENSE) 。 331 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fuzi (斧子) 2 | 3 | [![Build Status](https://api.travis-ci.org/cezheng/Fuzi.svg)](https://travis-ci.org/cezheng/Fuzi) 4 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Fuzi.svg)](https://cocoapods.org/pods/Fuzi) 5 | [![License](https://img.shields.io/cocoapods/l/Fuzi.svg?style=flat&color=gray)](http://opensource.org/licenses/MIT) 6 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![Platform](https://img.shields.io/cocoapods/p/Fuzi.svg?style=flat)](http://cezheng.github.io/Fuzi/) 8 | [![Twitter](https://img.shields.io/badge/twitter-@AdamoCheng-blue.svg?style=flat)](http://twitter.com/AdamoCheng) 9 | 10 | **A fast & lightweight XML/HTML parser in Swift that makes your life easier.** [[Documentation]](http://cezheng.github.io/Fuzi/) 11 | 12 | Fuzi is based on a Swift port of Mattt Thompson's [Ono](https://github.com/mattt/Ono)(斧), using most of its low level implementations with moderate class & interface redesign following standard Swift conventions, along with several bug fixes. 13 | 14 | > Fuzi(斧子) means "axe", in homage to [Ono](https://github.com/mattt/Ono)(斧), which in turn is inspired by [Nokogiri](http://nokogiri.org) (鋸), which means "saw". 15 | 16 | [简体中文](README-zh.md) 17 | [日本語](README-ja.md) 18 | ## A Quick Look 19 | ```swift 20 | let xml = "..." 21 | // or 22 | // let xmlData = <some NSData or Data> 23 | do { 24 | let document = try XMLDocument(string: xml) 25 | // or 26 | // let document = try XMLDocument(data: xmlData) 27 | 28 | if let root = document.root { 29 | // Accessing all child nodes of root element 30 | for element in root.children { 31 | print("\(element.tag): \(element.attributes)") 32 | } 33 | 34 | // Getting child element by tag & accessing attributes 35 | if let length = root.firstChild(tag:"Length", inNamespace: "dc") { 36 | print(length["unit"]) // `unit` attribute 37 | print(length.attributes) // all attributes 38 | } 39 | } 40 | 41 | // XPath & CSS queries 42 | for element in document.xpath("//element") { 43 | print("\(element.tag): \(element.attributes)") 44 | } 45 | 46 | if let firstLink = document.firstChild(css: "a, link") { 47 | print(firstLink["href"]) 48 | } 49 | } catch let error { 50 | print(error) 51 | } 52 | ``` 53 | 54 | ## Features 55 | ### Inherited from Ono 56 | - Extremely performant document parsing and traversal, powered by `libxml2` 57 | - Support for both [XPath](http://en.wikipedia.org/wiki/XPath) and [CSS](http://en.wikipedia.org/wiki/Cascading_Style_Sheets) queries 58 | - Automatic conversion of date and number values 59 | - Correct, common-sense handling of XML namespaces for elements and attributes 60 | - Ability to load HTML and XML documents from either `String` or `NSData` or `[CChar]` 61 | - Comprehensive test suite 62 | - Full documentation 63 | 64 | ### Improved in Fuzi 65 | - Simple, modern API following standard Swift conventions, no more return types like `AnyObject!` that cause unnecessary type casts 66 | - Customizable date and number formatters 67 | - Some bugs fixes 68 | - More convenience methods for HTML Documents 69 | - Access XML nodes of all types (Including text, comment, etc.) 70 | - Support for more CSS selectors (yet to come) 71 | 72 | 73 | ## Requirements 74 | 75 | - iOS 8.0+ / Mac OS X 10.9+ 76 | - Xcode 8.0+ 77 | 78 | > Use version [0.4.0](../../releases/tag/0.4.0) for Swift 2.3. 79 | 80 | 81 | ## Installation 82 | 83 | There are 4 ways you can install Fuzi to your project. 84 | 85 | ### Using [CocoaPods](http://cocoapods.org/) 86 | You can use [CocoaPods](http://cocoapods.org/) to install `Fuzi` by adding it to your to your `Podfile`: 87 | 88 | ```ruby 89 | platform :ios, '8.0' 90 | use_frameworks! 91 | 92 | target 'MyApp' do 93 | pod 'Fuzi', '~> 1.0.0' 94 | end 95 | ``` 96 | 97 | Then, run the following command: 98 | 99 | ```bash 100 | $ pod install 101 | ``` 102 | 103 | ### Using Swift Package Manager 104 | The Swift Package Manager is now built-in with Xcode 11 (currently in beta). You can easily add Fuzi as a dependency by choosing `File > Swift Packages > Add Package Dependency...` or in the Swift Packages tab of your project file and clicking on `+`. 105 | Simply use `https://github.com/cezheng/Fuzi` as repository and Xcode should automatically resolve the current version. 106 | 107 | ### Manually 108 | 1. Add all `*.swift` files in `Fuzi` directory into your project. 109 | 2. In your Xcode project `Build Settings`: 110 | 1. Find `Search Paths`, add `$(SDKROOT)/usr/include/libxml2` to `Header Search Paths`. 111 | 2. Find `Linking`, add `-lxml2` to `Other Linker Flags`. 112 | 113 | ### Using [Carthage](https://github.com/Carthage/Carthage) 114 | Create a `Cartfile` or `Cartfile.private` in the root directory of your project, and add the following line: 115 | 116 | ``` 117 | github "cezheng/Fuzi" ~> 1.0.0 118 | ``` 119 | Run the following command: 120 | 121 | ``` 122 | $ carthage update 123 | ``` 124 | Then do the followings in Xcode: 125 | 126 | 1. Drag the `Fuzi.framework` built by Carthage into your target's `General` -> `Embedded Binaries`. 127 | 2. In `Build Settings`, find `Search Paths`, add `$(SDKROOT)/usr/include/libxml2` to `Header Search Paths`. 128 | 129 | 130 | ## Usage 131 | ### XML 132 | ```swift 133 | import Fuzi 134 | 135 | let xml = "..." 136 | do { 137 | // if encoding is omitted, it defaults to NSUTF8StringEncoding 138 | let document = try XMLDocument(string: html, encoding: String.Encoding.utf8) 139 | if let root = document.root { 140 | print(root.tag) 141 | 142 | // define a prefix for a namespace 143 | document.definePrefix("atom", defaultNamespace: "http://www.w3.org/2005/Atom") 144 | 145 | // get first child element with given tag in namespace(optional) 146 | print(root.firstChild(tag: "title", inNamespace: "atom")) 147 | 148 | // iterate through all children 149 | for element in root.children { 150 | print("\(index) \(element.tag): \(element.attributes)") 151 | } 152 | } 153 | // you can also use CSS selector against XMLDocument when you feels it makes sense 154 | } catch let error as XMLError { 155 | switch error { 156 | case .noError: print("wth this should not appear") 157 | case .parserFailure, .invalidData: print(error) 158 | case .libXMLError(let code, let message): 159 | print("libxml error code: \(code), message: \(message)") 160 | } 161 | } 162 | ``` 163 | ### HTML 164 | `HTMLDocument` is a subclass of `XMLDocument`. 165 | 166 | ```swift 167 | import Fuzi 168 | 169 | let html = "<html>...</html>" 170 | do { 171 | // if encoding is omitted, it defaults to NSUTF8StringEncoding 172 | let doc = try HTMLDocument(string: html, encoding: String.Encoding.utf8) 173 | 174 | // CSS queries 175 | if let elementById = doc.firstChild(css: "#id") { 176 | print(elementById.stringValue) 177 | } 178 | for link in doc.css("a, link") { 179 | print(link.rawXML) 180 | print(link["href"]) 181 | } 182 | 183 | // XPath queries 184 | if let firstAnchor = doc.firstChild(xpath: "//body/a") { 185 | print(firstAnchor["href"]) 186 | } 187 | for script in doc.xpath("//head/script") { 188 | print(script["src"]) 189 | } 190 | 191 | // Evaluate XPath functions 192 | if let result = doc.eval(xpath: "count(/*/a)") { 193 | print("anchor count : \(result.doubleValue)") 194 | } 195 | 196 | // Convenient HTML methods 197 | print(doc.title) // gets <title>'s innerHTML in <head> 198 | print(doc.head) // gets <head> element 199 | print(doc.body) // gets <body> element 200 | 201 | } catch let error { 202 | print(error) 203 | } 204 | ``` 205 | 206 | ### I don't care about error handling 207 | 208 | ```swift 209 | import Fuzi 210 | 211 | let xml = "..." 212 | 213 | // Don't show me the errors, just don't crash 214 | if let doc1 = try? XMLDocument(string: xml) { 215 | //... 216 | } 217 | 218 | let html = "<html>...</html>" 219 | 220 | // I'm sure this won't crash 221 | let doc2 = try! HTMLDocument(string: html) 222 | //... 223 | ``` 224 | 225 | ### I want to access Text Nodes 226 | Not only text nodes, you can specify what types of nodes you would like to access. 227 | 228 | ```swift 229 | let document = ... 230 | // Get all child nodes that are Element nodes, Text nodes, or Comment nodes 231 | document.root?.childNodes(ofTypes: [.Element, .Text, .Comment]) 232 | ``` 233 | 234 | ## Migrating From Ono? 235 | Looking at example programs is the swiftest way to know the difference. The following 2 examples do exactly the same thing. 236 | 237 | [Ono Example](https://github.com/mattt/Ono/blob/master/Example/main.m) 238 | 239 | [Fuzi Example](FuziDemo/FuziDemo/main.swift) 240 | 241 | ### Accessing children 242 | **Ono** 243 | 244 | ```objc 245 | [doc firstChildWithTag:tag inNamespace:namespace]; 246 | [doc firstChildWithXPath:xpath]; 247 | [doc firstChildWithXPath:css]; 248 | for (ONOXMLElement *element in parent.children) { 249 | //... 250 | } 251 | [doc childrenWithTag:tag inNamespace:namespace]; 252 | ``` 253 | **Fuzi** 254 | 255 | ```swift 256 | doc.firstChild(tag: tag, inNamespace: namespace) 257 | doc.firstChild(xpath: xpath) 258 | doc.firstChild(css: css) 259 | for element in parent.children { 260 | //... 261 | } 262 | doc.children(tag: tag, inNamespace:namespace) 263 | ``` 264 | ### Iterate through query results 265 | **Ono** 266 | 267 | Conforms to `NSFastEnumeration`. 268 | 269 | ```objc 270 | // simply iterating through the results 271 | // mark `__unused` to unused params `idx` and `stop` 272 | [doc enumerateElementsWithXPath:xpath usingBlock:^(ONOXMLElement *element, __unused NSUInteger idx, __unused BOOL *stop) { 273 | NSLog(@"%@", element); 274 | }]; 275 | 276 | // stop the iteration at second element 277 | [doc enumerateElementsWithXPath:XPath usingBlock:^(ONOXMLElement *element, NSUInteger idx, BOOL *stop) { 278 | *stop = (idx == 1); 279 | }]; 280 | 281 | // getting element by index 282 | ONOXMLDocument *nthElement = [(NSEnumerator*)[doc CSS:css] allObjects][n]; 283 | 284 | // total element count 285 | NSUInteger count = [(NSEnumerator*)[document XPath:xpath] allObjects].count; 286 | ``` 287 | 288 | **Fuzi** 289 | 290 | Conforms to Swift's `SequenceType` and `Indexable`. 291 | 292 | ```swift 293 | // simply iterating through the results 294 | // no need to write the unused `idx` or `stop` params 295 | for element in doc.xpath(xpath) { 296 | print(element) 297 | } 298 | 299 | // stop the iteration at second element 300 | for (index, element) in doc.xpath(xpath).enumerate() { 301 | if idx == 1 { 302 | break 303 | } 304 | } 305 | 306 | // getting element by index 307 | if let nthElement = doc.css(css)[n] { 308 | //... 309 | } 310 | 311 | // total element count 312 | let count = doc.xpath(xpath).count 313 | ``` 314 | 315 | ### Evaluating XPath Functions 316 | **Ono** 317 | 318 | ```objc 319 | ONOXPathFunctionResult *result = [doc functionResultByEvaluatingXPath:xpath]; 320 | result.boolValue; //BOOL 321 | result.numericValue; //double 322 | result.stringValue; //NSString 323 | ``` 324 | 325 | **Fuzi** 326 | 327 | ```swift 328 | if let result = doc.eval(xpath: xpath) { 329 | result.boolValue //Bool 330 | result.doubleValue //Double 331 | result.stringValue //String 332 | } 333 | ``` 334 | 335 | ## License 336 | 337 | `Fuzi` is released under the MIT license. See [LICENSE](LICENSE) for details. 338 | -------------------------------------------------------------------------------- /Sources/Document.swift: -------------------------------------------------------------------------------- 1 | // Document.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import libxml2 24 | 25 | /// XML document which can be searched and queried. 26 | open class XMLDocument { 27 | // MARK: - Document Attributes 28 | /// The XML version. 29 | open fileprivate(set) lazy var version: String? = { 30 | return ^-^self.cDocument.pointee.version 31 | }() 32 | 33 | /// The string encoding for the document. This is NSUTF8StringEncoding if no encoding is set, or it cannot be calculated. 34 | open fileprivate(set) lazy var encoding: String.Encoding = { 35 | if let encodingName = ^-^self.cDocument.pointee.encoding { 36 | let encoding = CFStringConvertIANACharSetNameToEncoding(encodingName as CFString?) 37 | if encoding != kCFStringEncodingInvalidId { 38 | return String.Encoding(rawValue: UInt(CFStringConvertEncodingToNSStringEncoding(encoding))) 39 | } 40 | } 41 | return String.Encoding.utf8 42 | }() 43 | 44 | // MARK: - Accessing the Root Element 45 | /// The root element of the document. 46 | open fileprivate(set) var root: XMLElement? 47 | 48 | // MARK: - Accessing & Setting Document Formatters 49 | /// The formatter used to determine `numberValue` for elements in the document. By default, this is an `NSNumberFormatter` instance with `NSNumberFormatterDecimalStyle`. 50 | open lazy var numberFormatter: NumberFormatter = { 51 | let formatter = NumberFormatter() 52 | formatter.numberStyle = .decimal 53 | return formatter 54 | }() 55 | 56 | /// The formatter used to determine `dateValue` for elements in the document. By default, this is an `NSDateFormatter` instance configured to accept ISO 8601 formatted timestamps. 57 | open lazy var dateFormatter: DateFormatter = { 58 | let formatter = DateFormatter() 59 | formatter.locale = Locale(identifier: "en_US_POSIX") 60 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 61 | return formatter 62 | }() 63 | 64 | // MARK: - Creating XML Documents 65 | fileprivate let cDocument: xmlDocPtr 66 | 67 | /** 68 | Creates and returns an instance of XMLDocument from an XML string, throwing XMLError if an error occured while parsing the XML. 69 | 70 | - parameter string: The XML string. 71 | - parameter encoding: The string encoding. 72 | 73 | - throws: `XMLError` instance if an error occurred 74 | 75 | - returns: An `XMLDocument` with the contents of the specified XML string. 76 | */ 77 | public convenience init(string: String, encoding: String.Encoding = String.Encoding.utf8) throws { 78 | guard let cChars = string.cString(using: encoding) else { 79 | throw XMLError.invalidData 80 | } 81 | try self.init(cChars: cChars) 82 | } 83 | 84 | /** 85 | Creates and returns an instance of XMLDocument from XML data, throwing XMLError if an error occured while parsing the XML. 86 | 87 | - parameter data: The XML data. 88 | 89 | - throws: `XMLError` instance if an error occurred 90 | 91 | - returns: An `XMLDocument` with the contents of the specified XML string. 92 | */ 93 | public convenience init(data: Data) throws { 94 | let buffer = data.withUnsafeBytes { $0.bindMemory(to: Int8.self) } 95 | try self.init(buffer: buffer) 96 | } 97 | 98 | /** 99 | Creates and returns an instance of XMLDocument from C char array, throwing XMLError if an error occured while parsing the XML. 100 | 101 | - parameter cChars: cChars The XML data as C char array 102 | 103 | - throws: `XMLError` instance if an error occurred 104 | 105 | - returns: An `XMLDocument` with the contents of the specified XML string. 106 | */ 107 | public convenience init(cChars: [CChar]) throws { 108 | let buffer = cChars.withUnsafeBufferPointer { buffer in 109 | UnsafeBufferPointer(rebasing: buffer[0..<buffer.count]) 110 | } 111 | try self.init(buffer: buffer) 112 | } 113 | 114 | /** 115 | Creates and returns an instance of XMLDocument from C char buffer, throwing XMLError if an error occured while parsing the XML. 116 | 117 | - parameter buffer: The XML data as C char buffer 118 | 119 | - throws: `XMLError` instance if an error occurred 120 | 121 | - returns: An `XMLDocument` with the contents of the specified XML string. 122 | */ 123 | 124 | public convenience init(buffer: UnsafeBufferPointer<Int8>) throws { 125 | let options = Int32(XML_PARSE_NOWARNING.rawValue | XML_PARSE_NOERROR.rawValue | XML_PARSE_RECOVER.rawValue) 126 | try self.init(buffer: buffer, options: options) 127 | } 128 | 129 | fileprivate convenience init(buffer: UnsafeBufferPointer<Int8>, options: Int32) throws { 130 | guard let document = type(of: self).parse(buffer: buffer, options: options) else { 131 | throw XMLError.lastError(defaultError: .parserFailure) 132 | } 133 | xmlResetLastError() 134 | self.init(cDocument: document) 135 | } 136 | 137 | fileprivate class func parse(buffer: UnsafeBufferPointer<Int8>, options: Int32) -> xmlDocPtr? { 138 | return xmlReadMemory(buffer.baseAddress, Int32(buffer.count), "", nil, options) 139 | } 140 | 141 | fileprivate init(cDocument: xmlDocPtr) { 142 | self.cDocument = cDocument 143 | if let cRoot = xmlDocGetRootElement(cDocument) { 144 | root = XMLElement(cNode: cRoot, document: self) 145 | } 146 | } 147 | 148 | deinit { 149 | xmlFreeDoc(cDocument) 150 | } 151 | 152 | // MARK: - XML Namespaces 153 | var namespaces = [String: String]() // prefix -> URI 154 | 155 | /** 156 | Defines a new prefix for the given namespace in XPath expressions. 157 | 158 | - parameter prefix: The prefix name 159 | - parameter ns: The namespace URI declared in the XML Document 160 | */ 161 | open func definePrefix(_ prefix: String, forNamespace ns: String) { 162 | namespaces[prefix] = ns 163 | } 164 | 165 | /** 166 | Define a prefix for a default namespace. 167 | 168 | - parameter prefix: The prefix name 169 | - parameter ns: The default namespace URI that declared in XML Document 170 | */ 171 | @available(*, deprecated, renamed: "definePrefix(_:forNamespace:)", message: "This API will be removed in version 4.") 172 | open func definePrefix(_ prefix: String, defaultNamespace ns: String) { 173 | definePrefix(prefix, forNamespace: ns) 174 | } 175 | } 176 | 177 | extension XMLDocument: Equatable {} 178 | 179 | /** 180 | Determine whether two documents are the same 181 | 182 | - parameter lhs: XMLDocument on the left 183 | - parameter rhs: XMLDocument on the right 184 | 185 | - returns: whether lhs and rhs are equal 186 | */ 187 | public func ==(lhs: XMLDocument, rhs: XMLDocument) -> Bool { 188 | return lhs.cDocument == rhs.cDocument 189 | } 190 | 191 | /// HTML document which can be searched and queried. 192 | open class HTMLDocument: XMLDocument { 193 | // MARK: - Convenience Accessors 194 | 195 | /// HTML title of current document 196 | open var title: String? { 197 | return root?.firstChild(tag: "head")?.firstChild(tag: "title")?.stringValue 198 | } 199 | 200 | /// HTML head element of current document 201 | open var head: XMLElement? { 202 | return root?.firstChild(tag: "head") 203 | } 204 | 205 | /// HTML body element of current document 206 | open var body: XMLElement? { 207 | return root?.firstChild(tag: "body") 208 | } 209 | 210 | fileprivate override class func parse(buffer: UnsafeBufferPointer<Int8>, options: Int32) -> xmlDocPtr? { 211 | return htmlReadMemory(buffer.baseAddress, Int32(buffer.count), "", nil, options) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Sources/Element.swift: -------------------------------------------------------------------------------- 1 | // Element.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import libxml2 24 | 25 | /// Represents an element in `XMLDocument` or `HTMLDocument` 26 | open class XMLElement: XMLNode { 27 | 28 | /// The element's namespace. 29 | open fileprivate(set) lazy var namespace: String? = { 30 | return ^-^(self.cNode.pointee.ns != nil ?self.cNode.pointee.ns.pointee.prefix :nil) 31 | }() 32 | 33 | /// The element's tag. 34 | open fileprivate(set) lazy var tag: String? = { 35 | return ^-^self.cNode.pointee.name 36 | }() 37 | 38 | // MARK: - Accessing Attributes 39 | /// All attributes for the element. 40 | open fileprivate(set) lazy var attributes: [String : String] = { 41 | var attributes = [String: String]() 42 | var attribute = self.cNode.pointee.properties 43 | while attribute != nil { 44 | if let key = ^-^attribute?.pointee.name, let value = self.attr(key) { 45 | attributes[key] = value 46 | } 47 | attribute = attribute?.pointee.next 48 | } 49 | return attributes 50 | }() 51 | 52 | /** 53 | Returns the value for the attribute with the specified key. 54 | 55 | - parameter name: The attribute name. 56 | - parameter ns: The namespace, or `nil` by default if not using a namespace 57 | 58 | - returns: The attribute value, or `nil` if the attribute is not defined. 59 | */ 60 | open func attr(_ name: String, namespace ns: String? = nil) -> String? { 61 | var value: String? = nil 62 | 63 | let xmlValue: UnsafeMutablePointer<xmlChar>? 64 | if let ns = ns { 65 | xmlValue = xmlGetNsProp(cNode, name, ns) 66 | } else { 67 | xmlValue = xmlGetProp(cNode, name) 68 | } 69 | 70 | if let xmlValue = xmlValue { 71 | value = ^-^xmlValue 72 | xmlFree(xmlValue) 73 | } 74 | return value 75 | } 76 | 77 | // MARK: - Accessing Children 78 | 79 | /// The element's children elements. 80 | open var children: [XMLElement] { 81 | return LinkedCNodes(head: cNode.pointee.children).compactMap { 82 | XMLElement(cNode: $0, document: self.document) 83 | } 84 | } 85 | 86 | /** 87 | Get the element's child nodes of specified types 88 | 89 | - parameter types: type of nodes that should be fetched (e.g. .Element, .Text, .Comment) 90 | 91 | - returns: all children of specified types 92 | */ 93 | open func childNodes(ofTypes types: [XMLNodeType]) -> [XMLNode] { 94 | return LinkedCNodes(head: cNode.pointee.children, types: types).compactMap { node in 95 | switch node.pointee.type { 96 | case XMLNodeType.Element: 97 | return XMLElement(cNode: node, document: self.document) 98 | default: 99 | return XMLNode(cNode: node, document: self.document) 100 | } 101 | } 102 | } 103 | 104 | /** 105 | Returns the first child element with a tag, or `nil` if no such element exists. 106 | 107 | - parameter tag: The tag name. 108 | - parameter ns: The namespace, or `nil` by default if not using a namespace 109 | 110 | - returns: The child element. 111 | */ 112 | open func firstChild(tag: XMLCharsComparable, inNamespace ns: XMLCharsComparable? = nil) -> XMLElement? { 113 | var nodePtr = cNode.pointee.children 114 | while let cNode = nodePtr { 115 | if cXMLNode(nodePtr, matchesTag: tag, inNamespace: ns) { 116 | return XMLElement(cNode: cNode, document: self.document) 117 | } 118 | nodePtr = cNode.pointee.next 119 | } 120 | return nil 121 | } 122 | 123 | /// faster version of firstChild with string literals (explicitly typed as StaticString) 124 | open func firstChild(staticTag tag: StaticString, inNamespace ns: StaticString? = nil) -> XMLElement? { 125 | return firstChild(tag: tag, inNamespace: ns) 126 | } 127 | 128 | /** 129 | Returns all children elements with the specified tag. 130 | 131 | - parameter tag: The tag name. 132 | - parameter ns: The namepsace, or `nil` by default if not using a namespace 133 | 134 | - returns: The children elements. 135 | */ 136 | open func children(tag: XMLCharsComparable, inNamespace ns: XMLCharsComparable? = nil) -> [XMLElement] { 137 | return LinkedCNodes(head: cNode.pointee.children).compactMap { 138 | cXMLNode($0, matchesTag: tag, inNamespace: ns) 139 | ? XMLElement(cNode: $0, document: self.document) : nil 140 | } 141 | } 142 | 143 | /// faster version of children with string literals (explicitly typed as StaticString) 144 | open func children(staticTag tag: StaticString, inNamespace ns: StaticString? = nil) -> [XMLElement] { 145 | return children(tag: tag, inNamespace: ns) 146 | } 147 | 148 | // MARK: - Accessing Content 149 | /// Whether the element has a value. 150 | open var isBlank: Bool { 151 | return stringValue.isEmpty 152 | } 153 | 154 | /// A number representation of the element's value, which is generated from the document's `numberFormatter` property. 155 | open fileprivate(set) lazy var numberValue: NSNumber? = { 156 | return self.document.numberFormatter.number(from: self.stringValue) 157 | }() 158 | 159 | /// A date representation of the element's value, which is generated from the document's `dateFormatter` property. 160 | open fileprivate(set) lazy var dateValue: Date? = { 161 | return self.document.dateFormatter.date(from: self.stringValue) 162 | }() 163 | 164 | /** 165 | Returns the child element at the specified index. 166 | 167 | - parameter idx: The index. 168 | 169 | - returns: The child element. 170 | */ 171 | open subscript (idx: Int) -> XMLElement? { 172 | return children[idx] 173 | } 174 | 175 | /** 176 | Returns the value for the attribute with the specified key. 177 | 178 | - parameter name: The attribute name. 179 | 180 | - returns: The attribute value, or `nil` if the attribute is not defined. 181 | */ 182 | open subscript (name: String) -> String? { 183 | return attr(name) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Sources/Error.swift: -------------------------------------------------------------------------------- 1 | // Error.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import libxml2 24 | 25 | /** 26 | * XMLError enumeration. 27 | */ 28 | public enum XMLError: Error { 29 | /// No error 30 | case noError 31 | /// Contains a libxml2 error with error code and message 32 | case libXMLError(code: Int, message: String) 33 | /// Failed to convert String to bytes using given string encoding 34 | case invalidData 35 | /// XML Parser failed to parse the document 36 | case parserFailure 37 | /// XPath has either syntax error or some unknown/unsupported function 38 | case xpathError(code: Int) 39 | 40 | internal static func lastError(defaultError: XMLError = .noError) -> XMLError { 41 | guard let errorPtr = xmlGetLastError() else { 42 | return defaultError 43 | } 44 | let message = (^-^errorPtr.pointee.message)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 45 | let code = Int(errorPtr.pointee.code) 46 | xmlResetError(errorPtr) 47 | return .libXMLError(code: code, message: message ?? "") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Fuzi.h: -------------------------------------------------------------------------------- 1 | // Fuzi.h 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #import <Foundation/Foundation.h> 23 | 24 | //! Project version number for Fuzi. 25 | FOUNDATION_EXPORT double FuziVersionNumber; 26 | 27 | //! Project version string for Fuzi. 28 | FOUNDATION_EXPORT const unsigned char FuziVersionString[]; 29 | -------------------------------------------------------------------------------- /Sources/Helpers.swift: -------------------------------------------------------------------------------- 1 | // Helpers.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import libxml2 24 | 25 | // Public Helpers 26 | 27 | /// For printing an `XMLNode` 28 | extension XMLNode: CustomStringConvertible, CustomDebugStringConvertible { 29 | /// String printed by `print` function 30 | public var description: String { 31 | return self.rawXML 32 | } 33 | 34 | /// String printed by `debugPrint` function 35 | public var debugDescription: String { 36 | return self.rawXML 37 | } 38 | } 39 | 40 | /// For printing an `XMLDocument` 41 | extension XMLDocument: CustomStringConvertible, CustomDebugStringConvertible { 42 | /// String printed by `print` function 43 | public var description: String { 44 | return self.root?.rawXML ?? "" 45 | } 46 | 47 | /// String printed by `debugPrint` function 48 | public var debugDescription: String { 49 | return self.root?.rawXML ?? "" 50 | } 51 | } 52 | 53 | /// Abstract key type for finding tags or other xml names, as used like element.children(tag: TagNameComparable, ...) 54 | public protocol XMLCharsComparable { 55 | func caseInsensitivelyEqual(to other: UnsafePointer<xmlChar>) -> Bool 56 | } 57 | 58 | /// For finding tags by normal string 59 | extension String: XMLCharsComparable { 60 | public func caseInsensitivelyEqual(to other: UnsafePointer<xmlChar>) -> Bool { 61 | return withCString { (cString: UnsafePointer<CChar>) in 62 | let xmlChars = UnsafeRawPointer(cString).assumingMemoryBound(to: xmlChar.self) 63 | return xmlStrcasecmp(xmlChars, other) == 0 64 | } 65 | } 66 | } 67 | 68 | /// For finding tags by string literals 69 | extension StaticString: XMLCharsComparable { 70 | // compare without copying to Swift.String memory 71 | public func caseInsensitivelyEqual(to other: UnsafePointer<xmlChar>) -> Bool { 72 | return withUTF8Buffer { (utf8Buffer: UnsafeBufferPointer<UInt8>) in 73 | guard let baseAddress = utf8Buffer.baseAddress else { 74 | return false 75 | } 76 | 77 | let xmlChars = UnsafeRawPointer(baseAddress).assumingMemoryBound(to: xmlChar.self) 78 | return xmlStrcasecmp(xmlChars, other) == 0 79 | } 80 | } 81 | } 82 | 83 | // Internal Helpers 84 | 85 | internal extension String { 86 | subscript (nsrange: NSRange) -> String { 87 | let start = utf16.index(utf16.startIndex, offsetBy: nsrange.location) 88 | let end = utf16.index(start, offsetBy: nsrange.length) 89 | return String(utf16[start..<end])! 90 | } 91 | } 92 | 93 | // Just a smiling helper operator making frequent UnsafePointer -> String cast 94 | 95 | prefix operator ^-^ 96 | internal prefix func ^-^ <T> (ptr: UnsafePointer<T>?) -> String? { 97 | if let ptr = ptr { 98 | return String(validatingUTF8: UnsafeRawPointer(ptr).assumingMemoryBound(to: CChar.self)) 99 | } 100 | return nil 101 | } 102 | 103 | internal prefix func ^-^ <T> (ptr: UnsafeMutablePointer<T>?) -> String? { 104 | if let ptr = ptr { 105 | return String(validatingUTF8: UnsafeMutableRawPointer(ptr).assumingMemoryBound(to: CChar.self)) 106 | } 107 | return nil 108 | } 109 | 110 | internal struct LinkedCNodes: Sequence, IteratorProtocol { 111 | internal let head: xmlNodePtr? 112 | internal let types: [xmlElementType] 113 | 114 | fileprivate var cursor: xmlNodePtr? 115 | mutating func next() -> xmlNodePtr? { 116 | defer { 117 | if let ptr = cursor { 118 | cursor = ptr.pointee.next 119 | } 120 | } 121 | while let ptr = cursor, !types.contains(where: { $0 == ptr.pointee.type }) { 122 | cursor = ptr.pointee.next 123 | } 124 | return cursor 125 | } 126 | 127 | init(head: xmlNodePtr?, types: [xmlElementType] = [XML_ELEMENT_NODE]) { 128 | self.head = head 129 | self.cursor = head 130 | self.types = types 131 | } 132 | } 133 | 134 | internal func cXMLNode(_ node: xmlNodePtr?, matchesTag tag: XMLCharsComparable, inNamespace ns: XMLCharsComparable?) -> Bool { 135 | guard let name = node?.pointee.name else { 136 | return false 137 | } 138 | 139 | var matches = tag.caseInsensitivelyEqual(to: name) 140 | 141 | if let ns = ns { 142 | guard let prefix = node?.pointee.ns?.pointee.prefix else { 143 | return false 144 | } 145 | matches = matches && ns.caseInsensitivelyEqual(to: prefix) 146 | } 147 | return matches 148 | } 149 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>$(EXECUTABLE_NAME)</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>$(PRODUCT_NAME)</string> 15 | <key>CFBundlePackageType</key> 16 | <string>FMWK</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>$(CURRENT_PROJECT_VERSION)</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>$(CURRENT_PROJECT_VERSION)</string> 23 | <key>NSHumanReadableCopyright</key> 24 | <string>Copyright © 2020 Ce Zheng. All rights reserved.</string> 25 | <key>NSPrincipalClass</key> 26 | <string></string> 27 | </dict> 28 | </plist> 29 | -------------------------------------------------------------------------------- /Sources/Node.swift: -------------------------------------------------------------------------------- 1 | // Node.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | 23 | import Foundation 24 | import libxml2 25 | 26 | /// Define a Swifty typealias for libxml's node type enum 27 | public typealias XMLNodeType = xmlElementType 28 | 29 | // MARK: - Give a Swifty name to each enum case of XMLNodeType 30 | extension XMLNodeType { 31 | /// Element 32 | public static var Element: xmlElementType { return XML_ELEMENT_NODE } 33 | /// Attribute 34 | public static var Attribute: xmlElementType { return XML_ATTRIBUTE_NODE } 35 | /// Text 36 | public static var Text: xmlElementType { return XML_TEXT_NODE } 37 | /// CData Section 38 | public static var CDataSection: xmlElementType { return XML_CDATA_SECTION_NODE } 39 | /// Entity Reference 40 | public static var EntityRef: xmlElementType { return XML_ENTITY_REF_NODE } 41 | /// Entity 42 | public static var Entity: xmlElementType { return XML_ENTITY_NODE } 43 | /// Pi 44 | public static var Pi: xmlElementType { return XML_PI_NODE } 45 | /// Comment 46 | public static var Comment: xmlElementType { return XML_COMMENT_NODE } 47 | /// Document 48 | public static var Document: xmlElementType { return XML_DOCUMENT_NODE } 49 | /// Document Type 50 | public static var DocumentType: xmlElementType { return XML_DOCUMENT_TYPE_NODE } 51 | /// Document Fragment 52 | public static var DocumentFrag: xmlElementType { return XML_DOCUMENT_FRAG_NODE } 53 | /// Notation 54 | public static var Notation: xmlElementType { return XML_NOTATION_NODE } 55 | /// HTML Document 56 | public static var HtmlDocument: xmlElementType { return XML_HTML_DOCUMENT_NODE } 57 | /// DTD 58 | public static var DTD: xmlElementType { return XML_DTD_NODE } 59 | /// Element Declaration 60 | public static var ElementDecl: xmlElementType { return XML_ELEMENT_DECL } 61 | /// Attribute Declaration 62 | public static var AttributeDecl: xmlElementType { return XML_ATTRIBUTE_DECL } 63 | /// Entity Declaration 64 | public static var EntityDecl: xmlElementType { return XML_ENTITY_DECL } 65 | /// Namespace Declaration 66 | public static var NamespaceDecl: xmlElementType { return XML_NAMESPACE_DECL } 67 | /// XInclude Start 68 | public static var XIncludeStart: xmlElementType { return XML_XINCLUDE_START } 69 | /// XInclude End 70 | public static var XIncludeEnd: xmlElementType { return XML_XINCLUDE_END } 71 | /// DocbDocument 72 | public static var DocbDocument: xmlElementType { return XML_DOCB_DOCUMENT_NODE } 73 | } 74 | 75 | infix operator ~= 76 | /** 77 | For supporting pattern matching of those enum case alias getters for XMLNodeType 78 | 79 | - parameter lhs: left hand side 80 | - parameter rhs: right hand side 81 | 82 | - returns: true if both sides equals 83 | */ 84 | public func ~=(lhs: XMLNodeType, rhs: XMLNodeType) -> Bool { 85 | return lhs.rawValue == rhs.rawValue 86 | } 87 | 88 | /// Base class for all XML nodes 89 | open class XMLNode { 90 | /// The document containing the element. 91 | public unowned let document: XMLDocument 92 | 93 | /// The type of the XMLNode 94 | open var type: XMLNodeType { 95 | return cNode.pointee.type 96 | } 97 | 98 | /// The element's line number. 99 | open fileprivate(set) lazy var lineNumber: Int = { 100 | return xmlGetLineNo(self.cNode) 101 | }() 102 | 103 | // MARK: - Accessing Parent and Sibling Elements 104 | /// The element's parent element. 105 | open fileprivate(set) lazy var parent: XMLElement? = { 106 | return XMLElement(cNode: self.cNode.pointee.parent, document: self.document) 107 | }() 108 | 109 | /// The element's previous sibling. 110 | open fileprivate(set) lazy var previousSibling: XMLElement? = { 111 | return XMLElement(cNode: self.cNode.pointee.prev, document: self.document) 112 | }() 113 | 114 | /// The element's next sibling. 115 | open fileprivate(set) lazy var nextSibling: XMLElement? = { 116 | return XMLElement(cNode: self.cNode.pointee.next, document: self.document) 117 | }() 118 | 119 | // MARK: - Accessing Contents 120 | /// Whether this is a HTML node 121 | open var isHTML: Bool { 122 | return UInt32(self.cNode.pointee.doc.pointee.properties) & XML_DOC_HTML.rawValue == XML_DOC_HTML.rawValue 123 | } 124 | 125 | /// A string representation of the element's value. 126 | open fileprivate(set) lazy var stringValue : String = { 127 | let key = xmlNodeGetContent(self.cNode) 128 | let stringValue = ^-^key ?? "" 129 | xmlFree(key) 130 | return stringValue 131 | }() 132 | 133 | /// The raw XML string of the element. 134 | open fileprivate(set) lazy var rawXML: String = { 135 | let buffer = xmlBufferCreate() 136 | if self.isHTML { 137 | htmlNodeDump(buffer, self.cNode.pointee.doc, self.cNode) 138 | } else { 139 | xmlNodeDump(buffer, self.cNode.pointee.doc, self.cNode, 0, 0) 140 | } 141 | let dumped = ^-^xmlBufferContent(buffer) ?? "" 142 | xmlBufferFree(buffer) 143 | return dumped 144 | }() 145 | 146 | /// Convert this node to XMLElement if it is an element node 147 | open func toElement() -> XMLElement? { 148 | return self as? XMLElement 149 | } 150 | 151 | internal let cNode: xmlNodePtr 152 | 153 | internal init(cNode: xmlNodePtr, document: XMLDocument) { 154 | self.cNode = cNode 155 | self.document = document 156 | } 157 | 158 | internal convenience init?(cNode: xmlNodePtr?, document: XMLDocument) { 159 | guard let cNode = cNode else { 160 | return nil 161 | } 162 | self.init(cNode: cNode, document: document) 163 | } 164 | } 165 | 166 | extension XMLNode: Equatable {} 167 | 168 | /** 169 | Determine whether two nodes are the same 170 | 171 | - parameter lhs: XMLNode on the left 172 | - parameter rhs: XMLNode on the right 173 | 174 | - returns: whether lhs and rhs are equal 175 | */ 176 | public func ==(lhs: XMLNode, rhs: XMLNode) -> Bool { 177 | return lhs.cNode == rhs.cNode 178 | } 179 | -------------------------------------------------------------------------------- /Sources/NodeSet.swift: -------------------------------------------------------------------------------- 1 | // NodeSet.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import libxml2 24 | 25 | /// An enumerable set of XML nodes 26 | open class NodeSet: Collection { 27 | // Index type for `Indexable` protocol 28 | public typealias Index = Int 29 | 30 | // IndexDistance type for `Indexable` protocol 31 | public typealias IndexDistance = Int 32 | 33 | fileprivate var cursor = 0 34 | open func next() -> XMLElement? { 35 | defer { 36 | cursor += 1 37 | } 38 | if cursor < self.count { 39 | return self[cursor] 40 | } 41 | return nil 42 | } 43 | 44 | /// Number of nodes 45 | open fileprivate(set) lazy var count: Int = { 46 | return Int(self.cNodeSet?.pointee.nodeNr ?? 0) 47 | }() 48 | 49 | /// First Element 50 | open var first: XMLElement? { 51 | return count > 0 ? self[startIndex] : nil 52 | } 53 | 54 | /// if nodeset is empty 55 | open var isEmpty: Bool { 56 | return (cNodeSet == nil) || (cNodeSet!.pointee.nodeNr == 0) || (cNodeSet!.pointee.nodeTab == nil) 57 | } 58 | 59 | /// Start index 60 | open var startIndex: Index { 61 | return 0 62 | } 63 | 64 | /// End index 65 | open var endIndex: Index { 66 | return count 67 | } 68 | 69 | /** 70 | Get the Nth node from set. 71 | 72 | - parameter idx: node index 73 | 74 | - returns: the idx'th node, nil if out of range 75 | */ 76 | open subscript(_ idx: Index) -> XMLElement { 77 | precondition(idx >= startIndex && idx < endIndex, "Index of out bound") 78 | return XMLElement(cNode: (cNodeSet!.pointee.nodeTab[idx])!, document: document) 79 | } 80 | 81 | /** 82 | Get the index after `idx` 83 | 84 | - parameter idx: node index 85 | 86 | - returns: the index after `idx` 87 | */ 88 | open func index(after idx: Index) -> Index { 89 | return idx + 1 90 | } 91 | 92 | internal let cNodeSet: xmlNodeSetPtr? 93 | internal let document: XMLDocument! 94 | 95 | internal init(cNodeSet: xmlNodeSetPtr?, document: XMLDocument?) { 96 | self.cNodeSet = cNodeSet 97 | self.document = document 98 | } 99 | } 100 | 101 | /// XPath selector result node set 102 | open class XPathNodeSet: NodeSet { 103 | /// Empty node set 104 | public static let emptySet = XPathNodeSet(cXPath: nil, document: nil) 105 | 106 | fileprivate var cXPath: xmlXPathObjectPtr? 107 | 108 | internal init(cXPath: xmlXPathObjectPtr?, document: XMLDocument?) { 109 | self.cXPath = cXPath 110 | let nodeSet = cXPath?.pointee.nodesetval 111 | super.init(cNodeSet: nodeSet, document: document) 112 | } 113 | 114 | deinit { 115 | if cXPath != nil { 116 | xmlXPathFreeObject(cXPath) 117 | } 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /Sources/Queryable.swift: -------------------------------------------------------------------------------- 1 | // Queryable.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import libxml2 24 | 25 | /** 26 | * The `Queryable` protocol is adopted by `XMLDocument`, `HTMLDocument` and `XMLElement`, denoting that they can search for elements using XPath or CSS selectors. 27 | */ 28 | public protocol Queryable { 29 | /** 30 | Returns the results for an XPath selector. 31 | 32 | - parameter xpath: XPath selector string. 33 | 34 | - returns: An enumerable collection of results. 35 | */ 36 | func xpath(_ xpath: String) -> NodeSet 37 | 38 | /** 39 | Returns the results for an XPath selector. 40 | 41 | - parameter xpath: XPath selector string. 42 | 43 | - returns: An enumerable collection of results. 44 | 45 | - Throws: last registered XMLError, most likely libXMLError with code and message. 46 | */ 47 | func tryXPath(_ xpath: String) throws -> NodeSet 48 | 49 | /** 50 | Returns the first elements matching an XPath selector, or `nil` if there are no results. 51 | 52 | - parameter xpath: The XPath selector. 53 | 54 | - returns: The child element. 55 | */ 56 | func firstChild(xpath: String) -> XMLElement? 57 | 58 | /** 59 | Returns the results for a CSS selector. 60 | 61 | - parameter css: The CSS selector string. 62 | 63 | - returns: An enumerable collection of results. 64 | */ 65 | func css(_ css: String) -> NodeSet 66 | 67 | /** 68 | Returns the first elements matching an CSS selector, or `nil` if there are no results. 69 | 70 | - parameter css: The CSS selector. 71 | 72 | - returns: The child element. 73 | */ 74 | func firstChild(css: String) -> XMLElement? 75 | 76 | /** 77 | Returns the result for evaluating an XPath selector that contains XPath function. 78 | 79 | - parameter xpath: The XPath query string. 80 | 81 | - returns: The eval function result. 82 | */ 83 | func eval(xpath: String) -> XPathFunctionResult? 84 | } 85 | 86 | /// Result for evaluating a XPath expression 87 | open class XPathFunctionResult { 88 | /// Boolean value 89 | open fileprivate(set) lazy var boolValue: Bool = { 90 | return self.cXPath.pointee.boolval != 0 91 | }() 92 | 93 | /// Double value 94 | open fileprivate(set) lazy var doubleValue: Double = { 95 | return self.cXPath.pointee.floatval 96 | }() 97 | 98 | /// String value 99 | open fileprivate(set) lazy var stringValue: String = { 100 | return ^-^self.cXPath.pointee.stringval ?? "" 101 | }() 102 | 103 | fileprivate let cXPath: xmlXPathObjectPtr 104 | internal init?(cXPath: xmlXPathObjectPtr?) { 105 | guard let cXPath = cXPath else { 106 | return nil 107 | } 108 | self.cXPath = cXPath 109 | } 110 | 111 | deinit { 112 | xmlXPathFreeObject(cXPath) 113 | } 114 | } 115 | 116 | extension XMLDocument: Queryable { 117 | /** 118 | Returns the results for an XPath selector. 119 | 120 | - parameter xpath: XPath selector string. 121 | 122 | - returns: An enumerable collection of results. 123 | */ 124 | public func xpath(_ xpath: String) -> NodeSet { 125 | return root == nil ?XPathNodeSet.emptySet :root!.xpath(xpath) 126 | } 127 | 128 | /** 129 | - parameter xpath: XPath selector string. 130 | 131 | - returns: An enumerable collection of results. 132 | 133 | - Throws: last registered XMLError, most likely libXMLError with code and message. 134 | */ 135 | public func tryXPath(_ xpath: String) throws -> NodeSet { 136 | guard let rootNode = root else { 137 | return XPathNodeSet.emptySet 138 | } 139 | 140 | return try rootNode.tryXPath(xpath) 141 | } 142 | /** 143 | Returns the first elements matching an XPath selector, or `nil` if there are no results. 144 | 145 | - parameter xpath: The XPath selector. 146 | 147 | - returns: The child element. 148 | */ 149 | public func firstChild(xpath: String) -> XMLElement? { 150 | return root?.firstChild(xpath: xpath) 151 | } 152 | 153 | /** 154 | Returns the results for a CSS selector. 155 | 156 | - parameter css: The CSS selector string. 157 | 158 | - returns: An enumerable collection of results. 159 | */ 160 | public func css(_ css: String) -> NodeSet { 161 | return root == nil ?XPathNodeSet.emptySet :root!.css(css) 162 | } 163 | 164 | /** 165 | Returns the first elements matching an CSS selector, or `nil` if there are no results. 166 | 167 | - parameter css: The CSS selector. 168 | 169 | - returns: The child element. 170 | */ 171 | public func firstChild(css: String) -> XMLElement? { 172 | return root?.firstChild(css: css) 173 | } 174 | 175 | /** 176 | Returns the result for evaluating an XPath selector that contains XPath function. 177 | 178 | - parameter xpath: The XPath query string. 179 | 180 | - returns: The eval function result. 181 | */ 182 | public func eval(xpath: String) -> XPathFunctionResult? { 183 | return root?.eval(xpath: xpath) 184 | } 185 | } 186 | 187 | extension XMLElement: Queryable { 188 | /** 189 | Returns the results for an XPath selector. 190 | 191 | - parameter xpath: XPath selector string. 192 | 193 | - returns: An enumerable collection of results. 194 | */ 195 | public func xpath(_ xpath: String) -> NodeSet { 196 | guard let cXPath = try? self.cXPath(xpathString: xpath) else { 197 | return XPathNodeSet.emptySet 198 | } 199 | return XPathNodeSet(cXPath: cXPath, document: document) 200 | } 201 | 202 | /** 203 | - parameter xpath: XPath selector string. 204 | 205 | - returns: An enumerable collection of results. 206 | 207 | - Throws: last registered XMLError, most likely libXMLError with code and message. 208 | */ 209 | public func tryXPath(_ xpath: String) throws -> NodeSet { 210 | return XPathNodeSet(cXPath: try self.cXPath(xpathString: xpath), document: document) 211 | } 212 | /** 213 | Returns the first elements matching an XPath selector, or `nil` if there are no results. 214 | 215 | - parameter xpath: The XPath selector. 216 | 217 | - returns: The child element. 218 | */ 219 | public func firstChild(xpath: String) -> XMLElement? { 220 | return self.xpath(xpath).first 221 | } 222 | 223 | /** 224 | Returns the results for a CSS selector. 225 | 226 | - parameter css: The CSS selector string. 227 | 228 | - returns: An enumerable collection of results. 229 | */ 230 | public func css(_ css: String) -> NodeSet { 231 | return xpath(XPath(fromCSS:css)) 232 | } 233 | 234 | /** 235 | Returns the first elements matching an CSS selector, or `nil` if there are no results. 236 | 237 | - parameter css: The CSS selector. 238 | 239 | - returns: The child element. 240 | */ 241 | public func firstChild(css: String) -> XMLElement? { 242 | return self.css(css).first 243 | } 244 | 245 | /** 246 | Returns the result for evaluating an XPath selector that contains XPath function. 247 | 248 | - parameter xpath: The XPath query string. 249 | 250 | - returns: The eval function result. 251 | */ 252 | public func eval(xpath: String) -> XPathFunctionResult? { 253 | guard let cXPath = try? cXPath(xpathString: xpath) else { 254 | return nil 255 | } 256 | return XPathFunctionResult(cXPath: cXPath) 257 | } 258 | 259 | fileprivate func cXPath(xpathString: String) throws -> xmlXPathObjectPtr { 260 | guard let context = xmlXPathNewContext(cNode.pointee.doc) else { 261 | throw XMLError.lastError(defaultError: .xpathError(code: 1207)) 262 | } 263 | 264 | func withXMLChar(_ string: String, _ handler: (UnsafePointer<xmlChar>) -> Void) { 265 | string.utf8CString 266 | .map { xmlChar(bitPattern: $0) } 267 | .withUnsafeBufferPointer { 268 | handler($0.baseAddress!) 269 | } 270 | } 271 | 272 | context.pointee.node = cNode 273 | 274 | // Registers namespace prefixes declared in the document. 275 | var node = cNode 276 | while node.pointee.parent != nil { 277 | var curNs = node.pointee.nsDef 278 | while let ns = curNs { 279 | if let prefix = ns.pointee.prefix { 280 | xmlXPathRegisterNs(context, prefix, ns.pointee.href) 281 | } 282 | curNs = ns.pointee.next 283 | } 284 | node = node.pointee.parent 285 | } 286 | 287 | // Registers additional namespace prefixes. 288 | for (prefix, uri) in document.namespaces { 289 | withXMLChar(prefix) { prefix in 290 | withXMLChar(uri) { uri in 291 | xmlXPathRegisterNs(context, prefix, uri) 292 | } 293 | } 294 | } 295 | 296 | defer { 297 | xmlXPathFreeContext(context) 298 | } 299 | guard let xmlXPath = xmlXPathEvalExpression(xpathString, context) else { 300 | throw XMLError.lastError(defaultError: .xpathError(code: 1207)) 301 | } 302 | return xmlXPath 303 | } 304 | } 305 | 306 | private class RegexConstants { 307 | static let idRegex = try! NSRegularExpression(pattern: "\\#([\\w-_]+)", options: []) 308 | 309 | static let classRegex = try! NSRegularExpression(pattern: "\\.([^\\.]+)", options: []) 310 | 311 | static let attributeRegex = try! NSRegularExpression(pattern: "\\[([^\\[\\]]+)\\]", options: []) 312 | } 313 | 314 | internal func XPath(fromCSS css: String) -> String { 315 | var xpathExpressions = [String]() 316 | for expression in css.components(separatedBy: ",") where !expression.isEmpty { 317 | var xpathComponents = ["./"] 318 | var prefix: String? = nil 319 | let expressionComponents = expression.trimmingCharacters(in: CharacterSet.whitespaces).components(separatedBy: CharacterSet.whitespaces) 320 | for (idx, var token) in expressionComponents.enumerated() { 321 | switch token { 322 | case "*" where idx != 0: xpathComponents.append("/*") 323 | case ">": prefix = "" 324 | case "+": prefix = "following-sibling::*[1]/self::" 325 | case "~": prefix = "following-sibling::" 326 | default: 327 | if prefix == nil && idx != 0 { 328 | prefix = "descendant::" 329 | } 330 | 331 | if let symbolRange = token.rangeOfCharacter(from: CharacterSet(charactersIn: "#.[]")) { 332 | let symbol = symbolRange.lowerBound == token.startIndex ?"*" :"" 333 | var xpathComponent = String(token[..<symbolRange.lowerBound]) 334 | let nsrange = NSRange(location: 0, length: token.utf16.count) 335 | 336 | if let result = RegexConstants.idRegex.firstMatch(in: token, options: [], range: nsrange), result.numberOfRanges > 1 { 337 | xpathComponent += "\(symbol)[@id = '\(token[result.range(at: 1)])']" 338 | } 339 | 340 | for result in RegexConstants.classRegex.matches(in: token, options: [], range: nsrange) where result.numberOfRanges > 1 { 341 | xpathComponent += "\(symbol)[contains(concat(' ',normalize-space(@class),' '),' \(token[result.range(at: 1)]) ')]" 342 | } 343 | 344 | for result in RegexConstants.attributeRegex.matches(in: token, options: [], range: nsrange) where result.numberOfRanges > 1 { 345 | xpathComponent += "[@\(token[result.range(at: 1)])]" 346 | } 347 | 348 | token = xpathComponent 349 | } 350 | 351 | if prefix != nil { 352 | token = prefix! + token 353 | prefix = nil 354 | } 355 | 356 | xpathComponents.append(token) 357 | } 358 | } 359 | xpathExpressions.append(xpathComponents.joined(separator: "/")) 360 | } 361 | return xpathExpressions.joined(separator: " | ") 362 | } 363 | -------------------------------------------------------------------------------- /Tests/AtomTests.swift: -------------------------------------------------------------------------------- 1 | // AtomTests.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import XCTest 23 | import Fuzi 24 | 25 | class AtomTests: XCTestCase { 26 | var document: Fuzi.XMLDocument! 27 | override func setUp() { 28 | super.setUp() 29 | let filePath = Bundle(for: AtomTests.self).url(forResource: "atom", withExtension: "xml")! 30 | do { 31 | document = try XMLDocument(data: Data(contentsOf: filePath)) 32 | } catch { 33 | XCTAssertFalse(true, "Error should not be thrown") 34 | } 35 | document.definePrefix("atom", forNamespace: "http://www.w3.org/2005/Atom") 36 | } 37 | 38 | func testXMLVersion() { 39 | XCTAssertEqual(document.version, "1.0", "XML version should be 1.0") 40 | } 41 | 42 | func testXMLEncoding() { 43 | XCTAssertEqual(document.encoding, String.Encoding.utf8, "XML encoding should be UTF-8") 44 | } 45 | 46 | func testRoot() { 47 | XCTAssertEqual(document.root?.tag, "feed", "root tag should be feed") 48 | } 49 | 50 | func testTitle() { 51 | let titleElement = document.root!.firstChild(tag: "title") 52 | XCTAssertNotNil(titleElement, "title element should not be nil") 53 | XCTAssertEqual(titleElement?.tag, "title", "tag should be `title`") 54 | XCTAssertEqual(titleElement?.stringValue, "Example Feed", "title string value should be 'Example Feed'") 55 | } 56 | 57 | func testXPathTitle() { 58 | let titleElement = document.root!.firstChild(xpath: "/atom:feed/atom:title") 59 | XCTAssertNotNil(titleElement, "title element should not be nil") 60 | XCTAssertEqual(titleElement?.tag, "title", "tag should be `title`") 61 | XCTAssertEqual(titleElement?.stringValue, "Example Feed", "title string value should be 'Example Feed'") 62 | } 63 | 64 | func testLinks() { 65 | let linkElements = self.document.root!.children(tag: "link") 66 | XCTAssertEqual(linkElements.count, 2, "should have 2 link elements") 67 | XCTAssertEqual(linkElements[0].stringValue, "", "stringValue should be empty") 68 | XCTAssertNotEqual(linkElements[0]["href"], linkElements[1]["href"], "href values should not be equal") 69 | } 70 | 71 | func testUpdated() { 72 | let updatedElement = document.root!.firstChild(tag: "updated") 73 | XCTAssertNotNil(updatedElement?.dateValue, "dateValue should not be nil") 74 | let calendar = Calendar(identifier: Calendar.Identifier.gregorian) 75 | let dateComponents = DateComponents( 76 | calendar: calendar, 77 | timeZone: TimeZone(abbreviation: "UTC"), 78 | year: 2003, 79 | month: 12, 80 | day: 13, 81 | hour: 18, 82 | minute: 30, 83 | second: 2 84 | ) 85 | XCTAssertEqual(updatedElement?.dateValue, dateComponents.date, "dateValue should be equal to December 13, 2003 6:30:02 PM") 86 | } 87 | 88 | func testEntries() { 89 | let entryElements = document.root!.children(tag: "entry") 90 | XCTAssertEqual(entryElements.count, 1, "should be 1 entry element") 91 | } 92 | 93 | func testNamespace() { 94 | let entryElements = document.root!.children(tag: "entry") 95 | XCTAssertEqual(entryElements.count, 1, "should be 1 entry element") 96 | 97 | let namespacedElements = entryElements.first!.children(tag: "language", inNamespace: "dc") 98 | XCTAssertEqual(namespacedElements.count, 1, "should be 1 entry element") 99 | 100 | let namespacedElement = namespacedElements.first! 101 | XCTAssertNotNil(namespacedElement.namespace, "the namespace shouldn't be nil") 102 | XCTAssertEqual(namespacedElement.namespace!, "dc", "Namespaces should match") 103 | } 104 | 105 | func testFirstChildInNameSpace() { 106 | let entryElement = document.root!.firstChild(tag: "entry") 107 | XCTAssertNotNil(entryElement, "the element shouldn't be nil") 108 | 109 | let namespacedElement = entryElement!.firstChild(tag: "language", inNamespace: "dc") 110 | XCTAssertNotNil(namespacedElement?.namespace, "the namespace shouldn't be nil") 111 | XCTAssertEqual(namespacedElement!.namespace, "dc", "Namespace should match") 112 | XCTAssertEqual(namespacedElement!.stringValue, "en-us", "value should match") 113 | } 114 | 115 | func testXPathWithNamespaces() { 116 | var count = 0 117 | for (offset, element) in document.xpath("//dc:language").enumerated() { 118 | XCTAssertNotNil(element.namespace, "the namespace shouldn't be nil") 119 | XCTAssertEqual(element.namespace!, "dc", "Namespaces should match") 120 | count = offset + 1 121 | } 122 | XCTAssertEqual(count, 1, "should be 1 entry element") 123 | } 124 | 125 | func testXPathWithNamespacesAliases() { 126 | document.definePrefix("atom-alias", forNamespace: "http://www.w3.org/2005/Atom") 127 | document.definePrefix("dc-alias", forNamespace: "http://purl.org/dc/elements/1.1/") 128 | 129 | var results = document.xpath("//atom-alias:entry/dc-alias:language") 130 | XCTAssertEqual(results.map { $0.rawXML }, ["<dc:language>en-us</dc:language>"]) 131 | XCTAssertEqual(results.first?.namespace, "dc", "The namespace should be the one declared in the document") 132 | 133 | results = document.xpath("//atom:entry/dc:language") 134 | XCTAssertEqual(results.map { $0.rawXML }, ["<dc:language>en-us</dc:language>"], "The default prefixes should still work") 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Tests/CSSTests.swift: -------------------------------------------------------------------------------- 1 | // CSSTests.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import XCTest 23 | @testable import Fuzi 24 | 25 | class CSSTests: XCTestCase { 26 | func testCSSWildcardSelector() { 27 | XCTAssertEqual(XPath(fromCSS:"*"), ".//*") 28 | } 29 | 30 | func testCSSElementSelector() { 31 | XCTAssertEqual(XPath(fromCSS:"div"), ".//div") 32 | } 33 | 34 | func testCSSClassSelector() { 35 | XCTAssertEqual(XPath(fromCSS:".highlighted"), ".//*[contains(concat(' ',normalize-space(@class),' '),' highlighted ')]") 36 | } 37 | 38 | func testCSSElementAndClassSelector() { 39 | XCTAssertEqual(XPath(fromCSS:"span.highlighted"), ".//span[contains(concat(' ',normalize-space(@class),' '),' highlighted ')]") 40 | } 41 | 42 | func testCSSElementAndIDSelector() { 43 | XCTAssertEqual(XPath(fromCSS:"h1#logo"), ".//h1[@id = 'logo']") 44 | } 45 | 46 | func testCSSIDSelector() { 47 | XCTAssertEqual(XPath(fromCSS:"#logo"), ".//*[@id = 'logo']") 48 | } 49 | 50 | func testCSSWildcardChildSelector() { 51 | XCTAssertEqual(XPath(fromCSS:"html *"), ".//html//*") 52 | } 53 | 54 | func testCSSDescendantCombinatorSelector() { 55 | XCTAssertEqual(XPath(fromCSS:"body p"), ".//body/descendant::p") 56 | } 57 | 58 | func testCSSChildCombinatorSelector() { 59 | XCTAssertEqual(XPath(fromCSS:"ul > li"), ".//ul/li") 60 | } 61 | 62 | func testCSSAdjacentSiblingCombinatorSelector() { 63 | XCTAssertEqual(XPath(fromCSS:"h1 + p"), ".//h1/following-sibling::*[1]/self::p") 64 | } 65 | 66 | func testCSSGeneralSiblingCombinatorSelector() { 67 | XCTAssertEqual(XPath(fromCSS:"p ~ p"), ".//p/following-sibling::p") 68 | } 69 | 70 | func testCSSMultipleExpressionSelector() { 71 | XCTAssertEqual(XPath(fromCSS:"img[alt]"), ".//img[@alt]") 72 | } 73 | 74 | func testCSSAttributeValueSelector() { 75 | XCTAssertEqual(XPath(fromCSS:"a[rel='next']"), ".//a[@rel='next']") 76 | } 77 | 78 | func testCSSMultipleAttributesSelector() { 79 | XCTAssertEqual(XPath(fromCSS:"a[rel='next'][href='/foo/bar']"), ".//a[@rel='next'][@href='/foo/bar']") 80 | } 81 | 82 | func testCSSAttributeSelector() { 83 | XCTAssertEqual(XPath(fromCSS:"ul, ol"), ".//ul | .//ol") 84 | } 85 | 86 | func testCSSIDCombinatorSelector() { 87 | XCTAssertEqual(XPath(fromCSS:"div#test .note"), ".//div[@id = 'test']/descendant::*[contains(concat(' ',normalize-space(@class),' '),' note ')]") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/DefaultNamespaceXPathTests.swift: -------------------------------------------------------------------------------- 1 | // DefaultNamespaceXPathTests.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import XCTest 23 | import Fuzi 24 | 25 | class DefaultNamespaceXPathTests: XCTestCase { 26 | var document: Fuzi.XMLDocument! 27 | override func setUp() { 28 | super.setUp() 29 | let filePath = Bundle(for: DefaultNamespaceXPathTests.self).url(forResource: "ocf", withExtension: "xml")! 30 | do { 31 | document = try XMLDocument(data: Data(contentsOf: filePath)) 32 | } catch { 33 | XCTAssertFalse(true, "Error should not be thrown") 34 | } 35 | } 36 | 37 | func testAbsoluteXPathWithDefaultNamespace() { 38 | document.definePrefix("ocf", forNamespace: "urn:oasis:names:tc:opendocument:xmlns:container") 39 | let xpath = "/ocf:container/ocf:rootfiles/ocf:rootfile" 40 | var count = 0 41 | for element in document.xpath(xpath) { 42 | XCTAssertEqual("rootfile", element.tag, "tag should be `rootfile`") 43 | count += 1 44 | } 45 | XCTAssertEqual(count, 1, "Element should be found at XPath \(xpath)") 46 | } 47 | 48 | func testRelativeXPathWithDefaultNamespace() { 49 | document.definePrefix("ocf", forNamespace: "urn:oasis:names:tc:opendocument:xmlns:container") 50 | let absoluteXPath = "/ocf:container/ocf:rootfiles" 51 | let relativeXPath = "./ocf:rootfile" 52 | var count = 0 53 | for absoluteElement in document.xpath(absoluteXPath) { 54 | for relativeElement in absoluteElement.xpath(relativeXPath) { 55 | XCTAssertEqual("rootfile", relativeElement.tag, "tag should be rootfile") 56 | count += 1 57 | } 58 | } 59 | XCTAssertEqual(count, 1, "Element should be found at XPath '\(relativeXPath)' relative to XPath '\(absoluteXPath)'") 60 | } 61 | 62 | func testDefaultNamespaceInChildNode() { 63 | document.definePrefix("ocf", forNamespace: "urn:oasis:names:tc:opendocument:xmlns:container") 64 | document.definePrefix("dc", forNamespace: "http://purl.org/dc/elements/1.1/") 65 | let results = document.xpath("/ocf:container/dc:metadata/dc:identifier") 66 | XCTAssertEqual(results.map { $0.rawXML }, ["<identifier id=\"pub-id\">urn:uuid:pubid</identifier>"]) 67 | XCTAssertNil(results.first?.namespace, "The namespace should be empty because none is declared in the document") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/HTMLTests.swift: -------------------------------------------------------------------------------- 1 | // HTMLTests.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import XCTest 23 | import Fuzi 24 | 25 | class HTMLTests: XCTestCase { 26 | var document: HTMLDocument! 27 | override func setUp() { 28 | super.setUp() 29 | let filePath = Bundle(for: HTMLTests.self).url(forResource: "web", withExtension: "html")! 30 | do { 31 | document = try HTMLDocument(data: Data(contentsOf: filePath)) 32 | } catch { 33 | XCTAssertFalse(true, "Error should not be thrown") 34 | } 35 | } 36 | 37 | func testRoot() { 38 | XCTAssertEqual(document.root!.tag, "html", "html not root element") 39 | } 40 | 41 | func testRootChildren() { 42 | let children = document.root?.children 43 | XCTAssertNotNil(children) 44 | XCTAssertEqual(children?.count, 2, "root element should have exactly two children") 45 | XCTAssertEqual(children?.first?.tag, "head", "head not first child of html") 46 | XCTAssertEqual(children?.last?.tag, "body", "body not last child of html") 47 | } 48 | 49 | func testTitleXPath() { 50 | var idx = 0 51 | for element in document.xpath("//head/title") { 52 | XCTAssertEqual(idx, 0, "more than one element found") 53 | XCTAssertEqual(element.stringValue, "mattt/Ono", "title mismatch") 54 | idx += 1 55 | } 56 | XCTAssertEqual(idx, 1, "should be exactly 1 element") 57 | } 58 | 59 | func testTitleCSS() { 60 | var idx = 0 61 | for element in document.css("head title") { 62 | XCTAssertEqual(idx, 0, "more than one element found") 63 | XCTAssertEqual(element.stringValue, "mattt/Ono", "title mismatch") 64 | idx += 1 65 | } 66 | XCTAssertEqual(idx, 1, "should be exactly 1 element") 67 | } 68 | 69 | func testIDCSS() { 70 | var idx = 0 71 | for element in document.css("#account_settings") { 72 | XCTAssertEqual(idx, 0, "more than one element found") 73 | XCTAssertEqual(element["href"], "/settings/profile", "href mismatch") 74 | idx += 1 75 | } 76 | XCTAssertEqual(idx, 1, "should be exactly 1 element") 77 | } 78 | 79 | func testThrowError() { 80 | do { 81 | document = try HTMLDocument(cChars: [CChar]()) 82 | XCTAssertFalse(true, "error should have been thrown") 83 | } catch XMLError.parserFailure { 84 | 85 | } catch { 86 | XCTAssertFalse(true, "error type should be ParserFailure") 87 | } 88 | } 89 | 90 | func testTitle() { 91 | XCTAssertEqual(document.title, "mattt/Ono", "title is not correct") 92 | } 93 | 94 | func testHead() { 95 | let head = document.head 96 | XCTAssertNotNil(head) 97 | XCTAssertEqual(head?.children(tag: "link").count, 13, "link element count is incorrect") 98 | XCTAssertEqual(head?.children(tag: "meta").count, 38, "meta element count is incorrect") 99 | let scripts = head?.children(tag: "script") 100 | XCTAssertEqual(scripts?.count, 2, "scripts count is incorrect") 101 | XCTAssertEqual(scripts?.first?["src"], "https://github.global.ssl.fastly.net/assets/frameworks-3d18c504ea97dc018d44d64d8fce147a96a944b8.js", "script 1's src is incorrect") 102 | XCTAssertEqual(scripts?.last?["src"], "https://github.global.ssl.fastly.net/assets/github-602f74794536bf3e30e883a2cf268ca8e05b651d.js", "script 2's src is incorrect") 103 | XCTAssertEqual(head?["prefix"], "og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# object: http://ogp.me/ns/object# article: http://ogp.me/ns/article# profile: http://ogp.me/ns/profile#", "prefix attribute value is incorrect") 104 | } 105 | 106 | func testBody() { 107 | let body = document.body 108 | XCTAssertNotNil(body) 109 | XCTAssertEqual(body?["class"], "logged_in env-production macintosh vis-public", "body class is incorrect") 110 | XCTAssertEqual(body?.children(tag: "div").count, 4, "div count is incorrect") 111 | } 112 | 113 | func testChildNodesWithElementsAndTextNodes() { 114 | let mixedNode = document.firstChild(css: "#ajax-error-message") 115 | let childNodes = mixedNode?.childNodes(ofTypes: [.Element, .Text]) 116 | XCTAssertEqual(childNodes?.count, 5, "should have 5 child nodes") 117 | XCTAssertEqual(childNodes?.compactMap { $0.toElement() }.count, 2, "should have 2 element nodes") 118 | XCTAssertEqual(childNodes?.compactMap { $0.type == .Element ? $0 : nil }.count, 2, "should have 2 element nodes") 119 | XCTAssertEqual(childNodes?.compactMap { $0.type == .Text ? $0 : nil }.count, 3, "should have 3 text nodes") 120 | } 121 | 122 | func testNextSiblingDoesNotCrash() { 123 | var child = document.root?.children.first 124 | while(child != nil) { 125 | child = child?.nextSibling 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>$(EXECUTABLE_NAME)</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>$(PRODUCT_NAME)</string> 15 | <key>CFBundlePackageType</key> 16 | <string>BNDL</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>$(CURRENT_PROJECT_VERSION)</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>$(CURRENT_PROJECT_VERSION)</string> 23 | </dict> 24 | </plist> 25 | -------------------------------------------------------------------------------- /Tests/Resources/atom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"> 3 | <title>Example Feed 4 | A subtitle. 5 | 6 | 7 | urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 8 | 2003-12-13T18:30:02Z 9 | 10 | Atom-Powered Robots Run Amok 11 | en-us 12 | 13 | 14 | 15 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 16 | 2003-12-13T18:30:02Z 17 | Some text. 18 | 19 | John Doe 20 | johndoe@example.com 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Tests/Resources/ocf.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | urn:uuid:pubid 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/Resources/vmap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/Resources/web.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | mattt/Ono 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Skip to content 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
79 |
80 | 81 | 88 | 89 | 90 | 91 |
92 | 93 | This repository 94 | 95 | 96 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 | 129 |
130 | 131 | 132 | 133 | 134 | 165 | 166 | 190 | 191 | 192 | 193 |
194 |
195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 |
203 |
204 | 205 |
206 |
207 | 208 | 209 |
    210 | 211 |
  • 212 |
    213 | 214 |
    215 | 218 | 219 | 220 | 221 | Watch 222 | 223 | 224 | 225 |
    226 | 276 |
    277 |
    278 | 279 |
    280 |
  • 281 | 282 |
  • 283 | 284 | 285 | 302 | 303 |
  • 304 | 305 | 306 |
  • 307 | 308 | Fork 309 | 310 | 311 |
  • 312 | 313 | 314 |
315 | 316 |

317 | public 318 | 319 | 320 | 321 | 322 | / 323 | Ono 324 | 325 | 326 | Octocat-spinner-32 327 | 328 | 329 |

330 |
331 |
332 | 333 |
334 |
335 |
336 | 337 | 338 |
339 |
340 | 368 |
369 | 389 | 390 | 391 |
392 | 401 |
402 |
403 | 404 |
405 | 406 | 407 | 408 | 409 |
412 |

HTTPS clone URL

413 |
414 | 416 | 417 | 418 |
419 |
420 | 421 | 422 | 423 |
426 |

SSH clone URL

427 |
428 | 430 | 431 | 432 |
433 |
434 | 435 | 436 | 437 |
440 |

Subversion checkout URL

441 |
442 | 444 | 445 | 446 |
447 |
448 | 449 | 450 |

You can clone with 451 | HTTPS, 452 | SSH, 453 | or Subversion. 454 | 455 | 456 | 457 | 458 | 459 |

460 | 461 | 462 | 463 | Clone in Desktop 464 | 465 | 466 | 467 | 472 | 473 | Download ZIP 474 | 475 |
476 |
477 | 478 |
479 | 480 | 481 | 482 | 483 |
484 |
485 |

A sensible way to deal with XML & HTML for iOS & Mac OS X

486 |
487 | 488 | 489 | Edit 490 |
491 | 492 |
493 | 494 | 495 |
496 | 497 |
498 | 499 | 500 |
501 | 502 | 503 | or cancel 504 |
505 |
506 | 507 |
508 | 509 | 572 | 573 |
574 | 575 | 582 | 583 | 584 |
586 |
587 | 588 | 589 |
590 | 591 | 592 | 593 | 594 | 595 | 596 |
597 | 601 | 602 | branch: 603 | master 604 | 605 | 606 | 688 |
689 | 690 | 691 | 692 |
693 | 694 | 695 | 696 | 698 |
699 | 700 | 701 |
702 |

703 | Merge pull request #4 from slightair/fixDescendantCombinatorSelectorP… 712 | 713 |

714 |
…roblem
715 | 
716 | Fix CSS descendant combinator selector problem
717 |
718 | 719 | latest commit 903780b268 720 | 721 |
722 | Mattt Thompson 723 | 724 | authored 725 | 726 |
727 |
728 |
729 | 730 | 731 | 732 | 733 | 735 | 736 | 740 | 743 | 754 | 755 | 756 | 757 | 761 | 764 | 767 | 768 | 769 | 770 | 774 | 777 | 788 | 789 | 790 | 791 | 795 | 798 | 801 | 802 | 803 | 804 | 808 | 811 | 814 | 815 | 816 | 817 | 821 | 824 | 827 | 828 | 829 | 830 | 831 |
737 | 738 | Octocat-spinner-32 739 | 741 | Example 742 | 744 | Merge pull request #4 from slightair/fixDescendantCombinatorSelectorP… 753 |
758 | 759 | Octocat-spinner-32 760 | 762 | Ono.xcworkspace 763 | 765 | Adding workspace 766 |
771 | 772 | Octocat-spinner-32 773 | 775 | Ono 776 | 778 | Merge pull request #4 from slightair/fixDescendantCombinatorSelectorP… 787 |
792 | 793 | Octocat-spinner-32 794 | 796 | LICENSE 797 | 799 | Initial Import 800 |
805 | 806 | Octocat-spinner-32 807 | 809 | Ono.podspec 810 | 812 | Bumping version to 0.0.2 813 |
818 | 819 | Octocat-spinner-32 820 | 822 | README.md 823 | 825 | Bumping version to 0.0.1 826 |
832 |
833 | 834 |
835 | 836 | 837 | README.md 838 | 839 | 840 |

841 | Ono (斧)

842 | 843 |

A sensible way to deal with XML & HTML for iOS & Mac OS X

844 | 845 |
846 |

Ono (斧) means "axe", in homage to Nokogiri (鋸), which means "saw".

847 |
848 | 849 |

850 | Usage

851 | 852 |
#import "Ono.h"
853 | 
854 | Data *data = ...;
855 | NSError *error;
856 | 
857 | ONOXMLDocument *document = [ONOXMLDocument XMLDocumentWithData:data error:&error];
858 | for (ONOXMLElement *element in document.rootElement.children) {
859 |     NSLog(@"%@: %@", element.tag, element.attributes);
860 | }
861 | 
862 | // Support for Namespaces
863 | NSString *author = [[document.rootElement firstChildWithTag:@"creator" inNamespace:@"dc"] stringValue];
864 | 
865 | // Automatic Conversion for Number & Date Values
866 | NSDate *date = [[document.rootElement firstChildWithTag:@"created_at"] dateValue]; // ISO 8601 Timestamp
867 | NSInteger numberOfWords = [[document.rootElement firstChildWithTag:@"word_count"] numberValue] integerValue];
868 | BOOL isPublished = [[document.rootElement firstChildWithTag:@"is_published"] numberValue] boolValue];
869 | 
870 | // Convenient Accessors for Attributes
871 | NSString *unit = [document.rootElement firstChildWithTag:@"Length"][@"unit"]
872 | NSDictionary *authorAttributes = [[document.rootElement firstChildWithTag:@"author"] attributes];
873 | 
874 | // Support for XPath & CSS Queries
875 | [document enumerateElementsWithXPath:@"//Content" block:^(ONOXMLElement *element) {
876 |     NSLog(@"%@", element);
877 | }];
878 | 
879 | 880 |

881 | Contact

882 | 883 |

Mattt Thompson 884 | @mattt

885 | 886 |

887 | License

888 | 889 |

Ono is available under the MIT license. See the LICENSE file for more info.

890 |
891 | 892 | 893 |
894 | 895 |
896 | 897 |
898 |
899 | 900 | 901 |
902 | 903 |
904 | 927 |
928 | 929 | 930 |
931 |
932 |
933 | 934 |
935 |
936 | 945 |
946 | 947 | 948 | 949 |
950 | 951 | 952 | Something went wrong with that request. Please try again. 953 |
954 | 955 | 956 | 957 | -------------------------------------------------------------------------------- /Tests/VMAPTests.swift: -------------------------------------------------------------------------------- 1 | // VMAPTests.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import XCTest 23 | import Fuzi 24 | 25 | class VMAPTests: XCTestCase { 26 | var document: Fuzi.XMLDocument! 27 | override func setUp() { 28 | super.setUp() 29 | let filePath = Bundle(for: VMAPTests.self).url(forResource: "vmap", withExtension: "xml")! 30 | do { 31 | document = try XMLDocument(data: Data(contentsOf: filePath)) 32 | } catch { 33 | XCTAssertFalse(true, "Error should not be thrown") 34 | } 35 | } 36 | 37 | func testAbsoluteXPathWithNamespace() { 38 | let xpath = "/vmap:VMAP/vmap:Extensions/uo:unicornOnce" 39 | var count = 0 40 | for element in document.xpath(xpath) { 41 | XCTAssertEqual("unicornOnce", element.tag, "tag should be `unicornOnce`") 42 | count += 1 43 | } 44 | XCTAssertEqual(count, 1, "Element should be found at XPath '\(xpath)'") 45 | } 46 | 47 | func testRelativeXPathWithNamespace() { 48 | let absoluteXPath = "/vmap:VMAP/vmap:Extensions" 49 | let relativeXPath = "./uo:unicornOnce" 50 | var count = 0 51 | for absoluteElement in document.xpath(absoluteXPath) { 52 | for relativeElement in absoluteElement.xpath(relativeXPath) { 53 | XCTAssertEqual("unicornOnce", relativeElement.tag, "tag should be `unicornOnce`") 54 | count += 1 55 | } 56 | } 57 | XCTAssertEqual(count, 1, "Element should be found at XPath '\(relativeXPath)' relative to XPath '\(absoluteXPath)'"); 58 | } 59 | 60 | func testUnicornOnceIsBlank() { 61 | let xpath = "/vmap:VMAP/vmap:Extensions/uo:unicornOnce" 62 | let element = document.firstChild(xpath: xpath) 63 | XCTAssertNotNil(element, "Element should not be nil") 64 | XCTAssertTrue(element!.isBlank, "Element should be blank") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/XMLTests.swift: -------------------------------------------------------------------------------- 1 | // XMLTests.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import XCTest 23 | import Fuzi 24 | 25 | class XMLTests: XCTestCase { 26 | var document: Fuzi.XMLDocument! 27 | override func setUp() { 28 | super.setUp() 29 | let filePath = Bundle(for: XMLTests.self).url(forResource: "xml", withExtension: "xml")! 30 | do { 31 | document = try XMLDocument(data: Data(contentsOf: filePath)) 32 | } catch { 33 | XCTAssertFalse(true, "Error should not be thrown") 34 | } 35 | } 36 | 37 | func testXMLVersion() { 38 | XCTAssertEqual(document.version, "1.0", "XML version should be 1.0") 39 | } 40 | 41 | func testXMLEncoding() { 42 | XCTAssertEqual(document.encoding, String.Encoding.utf8, "XML encoding should be UTF-8") 43 | } 44 | 45 | func testRoot() { 46 | XCTAssertEqual(document.root!.tag, "spec", "root tag should be spec") 47 | XCTAssertEqual(document.root!.attributes["w3c-doctype"], "rec", "w3c-doctype should be rec") 48 | XCTAssertEqual(document.root!.attributes["lang"], "en", "lang should be en") 49 | } 50 | 51 | func testTitle() { 52 | let titleElement = document.root!.firstChild(tag: "header")?.firstChild(tag: "title") 53 | XCTAssertNotNil(titleElement, "title element should not be nil") 54 | XCTAssertEqual(titleElement?.tag, "title", "tag should be `title`") 55 | XCTAssertEqual(titleElement?.stringValue, "Extensible Markup Language (XML)", "title string value should be 'Extensible Markup Language (XML)'") 56 | } 57 | 58 | func testXPath() { 59 | let path = "/spec/header/title" 60 | let elts = document.xpath(path) 61 | var counter = 0 62 | for elt in elts { 63 | XCTAssertEqual("title", elt.tag, "tag should be `title`") 64 | counter += 1 65 | } 66 | XCTAssertEqual(1, counter, "at least one element should have been found at element path '\(path)'") 67 | } 68 | 69 | func testTryXpathThrowsError() { 70 | do { 71 | _ = try document.tryXPath("////") 72 | XCTAssertFalse(true, "error should have been thrown") 73 | } catch XMLError.libXMLError(code: 1207, message: "Invalid expression") { 74 | 75 | } catch { 76 | XCTAssertFalse(true, "error type should be libXMLError \(error)") 77 | } 78 | } 79 | 80 | func testTryXpathFunctionThrowsError() { 81 | do { 82 | _ = try document.tryXPath("//*[unknown()]") 83 | XCTAssertFalse(true, "error should have been thrown") 84 | } catch XMLError.libXMLError(code: 1223, message: "Stack usage error") { 85 | 86 | } catch { 87 | XCTAssertFalse(true, "error type should be libXMLError \(error)") 88 | } 89 | } 90 | 91 | func testLineNumber() { 92 | let headerElement = document.root!.firstChild(tag: "header") 93 | XCTAssertNotNil(headerElement, "header element should not be nil") 94 | XCTAssertEqual(headerElement?.lineNumber, 123, "header line number should be correct") 95 | } 96 | 97 | func testThrowsError() { 98 | do { 99 | document = try XMLDocument(cChars: [CChar]()) 100 | XCTAssertFalse(true, "error should have been thrown") 101 | } catch XMLError.parserFailure { 102 | 103 | } catch { 104 | XCTAssertFalse(true, "error type should be ParserFailure") 105 | } 106 | } 107 | 108 | func testAuthorsByStaticTag() { 109 | let authlistElement = document.root!.firstChild(staticTag: "header")?.firstChild(staticTag: "authlist") 110 | XCTAssertNotNil(authlistElement, "authorlist element should not be nil") 111 | let authorElements = authlistElement?.children(staticTag: "author") 112 | XCTAssertEqual(authorElements?.count, 5, "should have 5 elements") 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tests/XPathFunctionResultTests.swift: -------------------------------------------------------------------------------- 1 | // XPathFunctionResultTests.swift 2 | // Copyright (c) 2015 Ce Zheng 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import XCTest 23 | import Fuzi 24 | 25 | class XPathFunctionResultTests: XCTestCase { 26 | var document: Fuzi.XMLDocument! 27 | override func setUp() { 28 | super.setUp() 29 | let filePath = Bundle(for: AtomTests.self).url(forResource: "atom", withExtension: "xml")! 30 | do { 31 | document = try XMLDocument(data: Data(contentsOf: filePath)) 32 | } catch { 33 | XCTAssertFalse(true, "Error should not be thrown") 34 | } 35 | document.definePrefix("atom", forNamespace: "http://www.w3.org/2005/Atom") 36 | } 37 | 38 | func testFunctionResultBoolValue() { 39 | XCTAssertTrue(document.root!.eval(xpath: "starts-with('Ono','O')")!.boolValue, "Result boolValue should be true") 40 | } 41 | 42 | func testFunctionResultDoubleValue() { 43 | XCTAssertEqual(document.root!.eval(xpath: "count(./atom:link)")!.doubleValue, 2, "Number of child links should be 2") 44 | } 45 | 46 | func testFunctionResultStringValue() { 47 | XCTAssertEqual(document.root!.eval(xpath: "string(./atom:entry[1]/dc:language[1]/text())")!.stringValue, "en-us", "Result stringValue should be `en-us`") 48 | } 49 | } 50 | --------------------------------------------------------------------------------