Skip to content
This repository has been archived by the owner on Oct 17, 2021. It is now read-only.

Commit

Permalink
Improve node traversal options (and more tweaks) (#14)
Browse files Browse the repository at this point in the history
* Make construct xmlNodePtr optional so that next/previous doesn't crash when no next/prev node

* Add more traversal options and make them lazy

* Fix element.parent to no crash when parent is nil

* Don't leak xmlNodeGetContent and xmlGetNodePath

* Use xml unset prop when setting element attribute to nil
  • Loading branch information
jessegrosjean authored Oct 17, 2021
1 parent 4bfa96f commit a8b350e
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 17 deletions.
11 changes: 9 additions & 2 deletions Sources/DOM/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ public final class Element: Node {
}

set {
xmlSetProp(xmlNode, attribute, newValue)
if let newValue = newValue {
xmlSetProp(xmlNode, attribute, newValue)
} else {
xmlUnsetProp(xmlNode, attribute)
}
}
}

Expand Down Expand Up @@ -79,7 +83,10 @@ public final class Element: Node {

// MARK: -

public required init?(rawValue: UnsafeMutableRawPointer) {
public required init?(rawValue: UnsafeMutableRawPointer?) {
guard let rawValue = rawValue else {
return nil
}
guard rawValue.bindMemory(to: _xmlNode.self, capacity: 1).pointee.type == XML_ELEMENT_NODE else { return nil }
super.init(rawValue: rawValue)
}
Expand Down
64 changes: 53 additions & 11 deletions Sources/DOM/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ open class Node: RawRepresentable, Equatable, Hashable, CustomStringConvertible

public var content: String? {
get {
return String(cString: xmlNodeGetContent(xmlNode))
return String(xmlString: xmlNodeGetContent(xmlNode))
}

set {
Expand All @@ -30,7 +30,7 @@ open class Node: RawRepresentable, Equatable, Hashable, CustomStringConvertible

public var xpath: String? {
guard let cString = xmlGetNodePath(xmlNode) else { return nil }
return String(cString: cString)
return String(xmlString: cString)
}

func unlink() {
Expand Down Expand Up @@ -69,36 +69,61 @@ open class Node: RawRepresentable, Equatable, Hashable, CustomStringConvertible
// MARK: -

public protocol Constructable {
static func construct(with rawValue: xmlNodePtr) -> Node?
static func construct(with rawValue: xmlNodePtr?) -> Node?
}

extension Constructable where Self: Node {

public var children: [Node] {
guard let firstChild = xmlNode.pointee.children else { return [] }
return sequence(first: firstChild, next: { $0.pointee.next })
.compactMap { Self.construct(with: $0) }
}


public var firstChildElement: Element? {
return findFirstNode(start: xmlNode.pointee.children, next: { $0.pointee.next }) { node in
node as? Element != nil
} as? Element
}

public func firstChildElement(named name: String) -> Element? {
for case let element as Element in children where element.name == name {
return element
}

return nil
return findFirstNode(start: xmlNode.pointee.children, next: { $0.pointee.next }) { node in
(node as? Element)?.name == name
} as? Element
}


public var lastChildElement: Element? {
return findFirstNode(start: lastChild?.xmlNode, next: { $0.pointee.prev }) { node in
(node as? Element) != nil
} as? Element
}

public func lastChildElement(named name: String) -> Element? {
return findFirstNode(start: lastChild?.xmlNode, next: { $0.pointee.prev }) { node in
(node as? Element)?.name == name
} as? Element
}

public var parent: Element? {
return Element(rawValue: xmlNode.pointee.parent)
}

public var previous: Node? {
return Self.construct(with: xmlNode.pointee.prev)
}

public var next: Node? {
return Self.construct(with: xmlNode.pointee.next)
}

public var firstChild: Node? {
return Self.construct(with: xmlNode.pointee.children)
}

public var lastChild: Node? {
return Self.construct(with: xmlGetLastChild(xmlNode))
}

@discardableResult
public func unwrap() -> Node? {
let children = sequence(first: xmlNode.pointee.children, next: { $0.pointee.next }).compactMap { Node(rawValue: $0) }
Expand All @@ -118,4 +143,21 @@ extension Constructable where Self: Node {

return children.first
}

private func findFirstNode(
start: UnsafeMutablePointer<_xmlNode>?,
next: @escaping (xmlNodePtr) -> xmlNodePtr?,
where predicate: (Node) -> Bool ) -> Node?
{
var n = start
while let each = n {
if let node = Self.construct(with: each) {
if predicate(node) {
return node
}
}
n = next(each)
}
return nil
}
}
6 changes: 5 additions & 1 deletion Sources/HTML/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import libxml2
import DOM

extension Node: Constructable {
public static func construct(with rawValue: xmlNodePtr) -> Node? {
public static func construct(with rawValue: xmlNodePtr?) -> Node? {
guard let rawValue = rawValue else {
return nil
}

switch rawValue.pointee.type {
case XML_ELEMENT_NODE:
return Element(rawValue: rawValue)
Expand Down
12 changes: 10 additions & 2 deletions Sources/XML/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ extension Element {

set {
if let namespace = namespace {
xmlSetNsProp(xmlNode, namespace.rawValue, attribute, newValue)
if let newValue = newValue {
xmlSetNsProp(xmlNode, namespace.rawValue, attribute, newValue)
} else {
xmlUnsetNsProp(xmlNode, namespace.rawValue, attribute)
}
} else {
xmlSetProp(xmlNode, attribute, newValue)
if let newValue = newValue {
xmlSetProp(xmlNode, attribute, newValue)
} else {
xmlUnsetProp(xmlNode, attribute)
}
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion Sources/XML/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import libxml2
import DOM

extension Node: Constructable {
public static func construct(with rawValue: xmlNodePtr) -> Node? {
public static func construct(with rawValue: xmlNodePtr?) -> Node? {
guard let rawValue = rawValue else {
return nil
}

switch rawValue.pointee.type {
case XML_ELEMENT_NODE:
return Element(rawValue: rawValue)
Expand Down
31 changes: 31 additions & 0 deletions Tests/XMLTests/XMLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,35 @@ final class XMLTests: XCTestCase {
XCTAssertEqual(results?.count, 1)
XCTAssertEqual(results?.first?["name"], "h1")
}

func testTraverse() throws {
let xml = #"""
<root>
hello
<one/>
<two/>
<three/>
world
</root>
"""#

let doc = try XML.Document(string: xml, options: [.removeBlankNodes])!
let root = doc.root!
XCTAssertEqual(root.name, "root")
XCTAssertNil(root.next)
XCTAssertNil(root.previous)
XCTAssertEqual(root.firstChildElement?.name, "one")
XCTAssertEqual(root.lastChildElement?.name, "three")
XCTAssertEqual(root.firstChild?.content?.trimmingCharacters(in: .whitespacesAndNewlines), "hello")
XCTAssertEqual(root.lastChild?.content?.trimmingCharacters(in: .whitespacesAndNewlines), "world")
XCTAssertNil(root.firstChild?.firstChild)
XCTAssertNil(root.firstChild?.lastChild)
XCTAssertNil(root.firstChildElement?.firstChild)
XCTAssertNil(root.firstChildElement?.lastChild)
}

func testParent() throws {
XCTAssertNil(Element(name: "test").parent)
}

}

0 comments on commit a8b350e

Please sign in to comment.