// // Node.swift // SwifSoup // // Created by Nabil Chatbi on 29/09/16. // Copyright © 2016 Nabil Chatbi.. All rights reserved. // import Foundation open class Node: Equatable, Hashable { private static let abs = "abs:" fileprivate static let empty = "" private static let EMPTY_NODES: Array = Array() weak var parentNode: Node? var childNodes: Array var attributes: Attributes? var baseUri: String? /** * Get the list index of this node in its node sibling list. I.e. if this is the first node * sibling, returns 0. * @return position in node sibling list * @see org.jsoup.nodes.Element#elementSiblingIndex() */ public private(set) var siblingIndex: Int = 0 /** Create a new Node. @param baseUri base URI @param attributes attributes (not null, but may be empty) */ public init(_ baseUri: String, _ attributes: Attributes) { self.childNodes = Node.EMPTY_NODES self.baseUri = baseUri.trim() self.attributes = attributes } public init(_ baseUri: String) { childNodes = Node.EMPTY_NODES self.baseUri = baseUri.trim() self.attributes = Attributes() } /** * Default constructor. Doesn't setup base uri, children, or attributes; use with caution. */ public init() { self.childNodes = Node.EMPTY_NODES self.attributes = nil self.baseUri = nil } /** Get the node name of this node. Use for debugging purposes and not logic switching (for that, use instanceof). @return node name */ public func nodeName() -> String { preconditionFailure("This method must be overridden") } /** * Get an attribute's value by its key. Case insensitive *

* To get an absolute URL from an attribute that may be a relative URL, prefix the key with abs, * which is a shortcut to the {@link #absUrl} method. *

* E.g.: *
String url = a.attr("abs:href");
* * @param attributeKey The attribute key. * @return The attribute, or empty string if not present (to avoid nulls). * @see #attributes() * @see #hasAttr(String) * @see #absUrl(String) */ open func attr(_ attributeKey: String)throws ->String { let val: String = try attributes!.getIgnoreCase(key: attributeKey) if (val.count > 0) { return val } else if (attributeKey.lowercased().startsWith(Node.abs)) { return try absUrl(attributeKey.substring(Node.abs.count)) } else {return Node.empty} } /** * Get all of the element's attributes. * @return attributes (which implements iterable, in same order as presented in original HTML). */ open func getAttributes() -> Attributes? { return attributes } /** * Set an attribute (key=value). If the attribute already exists, it is replaced. * @param attributeKey The attribute key. * @param attributeValue The attribute value. * @return this (for chaining) */ @discardableResult open func attr(_ attributeKey: String, _ attributeValue: String)throws->Node { try attributes?.put(attributeKey, attributeValue) return self } /** * Test if this element has an attribute. Case insensitive * @param attributeKey The attribute key to check. * @return true if the attribute exists, false if not. */ open func hasAttr(_ attributeKey: String) -> Bool { guard let attributes = attributes else { return false } if (attributeKey.startsWith(Node.abs)) { let key: String = attributeKey.substring(Node.abs.count) do { let abs = try absUrl(key) if (attributes.hasKeyIgnoreCase(key: key) && !Node.empty.equals(abs)) { return true } } catch { return false } } return attributes.hasKeyIgnoreCase(key: attributeKey) } /** * Remove an attribute from this element. * @param attributeKey The attribute to remove. * @return this (for chaining) */ @discardableResult open func removeAttr(_ attributeKey: String)throws->Node { try attributes?.removeIgnoreCase(key: attributeKey) return self } /** Get the base URI of this node. @return base URI */ open func getBaseUri() -> String { return baseUri! } /** Update the base URI of this node and all of its descendants. @param baseUri base URI to set */ open func setBaseUri(_ baseUri: String)throws { class nodeVisitor: NodeVisitor { private let baseUri: String init(_ baseUri: String) { self.baseUri = baseUri } func head(_ node: Node, _ depth: Int)throws { node.baseUri = baseUri } func tail(_ node: Node, _ depth: Int)throws { } } try traverse(nodeVisitor(baseUri)) } /** * Get an absolute URL from a URL attribute that may be relative (i.e. an <a href> or * <img src>). *

* E.g.: String absUrl = linkEl.absUrl("href"); *

*

* If the attribute value is already absolute (i.e. it starts with a protocol, like * http:// or https:// etc), and it successfully parses as a URL, the attribute is * returned directly. Otherwise, it is treated as a URL relative to the element's {@link #baseUri}, and made * absolute using that. *

*

* As an alternate, you can use the {@link #attr} method with the abs: prefix, e.g.: * String absUrl = linkEl.attr("abs:href"); *

* * @param attributeKey The attribute key * @return An absolute URL if one could be made, or an empty string (not null) if the attribute was missing or * could not be made successfully into a URL. * @see #attr * @see java.net.URL#URL(java.net.URL, String) */ open func absUrl(_ attributeKey: String)throws->String { try Validate.notEmpty(string: attributeKey) if (!hasAttr(attributeKey)) { return Node.empty // nothing to make absolute with } else { return StringUtil.resolve(baseUri!, relUrl: try attr(attributeKey)) } } /** Get a child node by its 0-based index. @param index index of child node @return the child node at this index. Throws a {@code IndexOutOfBoundsException} if the index is out of bounds. */ open func childNode(_ index: Int) -> Node { return childNodes[index] } /** Get this node's children. Presented as an unmodifiable list: new children can not be added, but the child nodes themselves can be manipulated. @return list of children. If no children, returns an empty list. */ open func getChildNodes()->Array { return childNodes } /** * Returns a deep copy of this node's children. Changes made to these nodes will not be reflected in the original * nodes * @return a deep copy of this node's children */ open func childNodesCopy()->Array { var children: Array = Array() for node: Node in childNodes { children.append(node.copy() as! Node) } return children } /** * Get the number of child nodes that this node holds. * @return the number of child nodes that this node holds. */ public func childNodeSize() -> Int { return childNodes.count } final func childNodesAsArray() -> [Node] { return childNodes as Array } /** Gets this node's parent node. @return parent node or null if no parent. */ open func parent() -> Node? { return parentNode } /** Gets this node's parent node. Node overridable by extending classes, so useful if you really just need the Node type. @return parent node or null if no parent. */ final func getParentNode() -> Node? { return parentNode } /** * Gets the Document associated with this Node. * @return the Document associated with this Node, or null if there is no such Document. */ open func ownerDocument() -> Document? { if let this = self as? Document { return this } else if (parentNode == nil) { return nil } else { return parentNode!.ownerDocument() } } /** * Remove (delete) this node from the DOM tree. If this node has children, they are also removed. */ open func remove()throws { try parentNode?.removeChild(self) } /** * Insert the specified HTML into the DOM before this node (i.e. as a preceding sibling). * @param html HTML to add before this node * @return this node, for chaining * @see #after(String) */ @discardableResult open func before(_ html: String)throws->Node { try addSiblingHtml(siblingIndex, html) return self } /** * Insert the specified node into the DOM before this node (i.e. as a preceding sibling). * @param node to add before this node * @return this node, for chaining * @see #after(Node) */ @discardableResult open func before(_ node: Node)throws ->Node { try Validate.notNull(obj: node) try Validate.notNull(obj: parentNode) try parentNode?.addChildren(siblingIndex, node) return self } /** * Insert the specified HTML into the DOM after this node (i.e. as a following sibling). * @param html HTML to add after this node * @return this node, for chaining * @see #before(String) */ @discardableResult open func after(_ html: String)throws ->Node { try addSiblingHtml(siblingIndex + 1, html) return self } /** * Insert the specified node into the DOM after this node (i.e. as a following sibling). * @param node to add after this node * @return this node, for chaining * @see #before(Node) */ @discardableResult open func after(_ node: Node)throws->Node { try Validate.notNull(obj: node) try Validate.notNull(obj: parentNode) try parentNode?.addChildren(siblingIndex+1, node) return self } private func addSiblingHtml(_ index: Int, _ html: String)throws { try Validate.notNull(obj: parentNode) let context: Element? = parent() as? Element let nodes: Array = try Parser.parseFragment(html, context, getBaseUri()) try parentNode?.addChildren(index, nodes) } /** * Insert the specified HTML into the DOM after this node (i.e. as a following sibling). * @param html HTML to add after this node * @return this node, for chaining * @see #before(String) */ @discardableResult open func after(html: String)throws->Node { try addSiblingHtml(siblingIndex + 1, html) return self } /** * Insert the specified node into the DOM after this node (i.e. as a following sibling). * @param node to add after this node * @return this node, for chaining * @see #before(Node) */ @discardableResult open func after(node: Node)throws->Node { try Validate.notNull(obj: node) try Validate.notNull(obj: parentNode) try parentNode?.addChildren(siblingIndex + 1, node) return self } open func addSiblingHtml(index: Int, _ html: String)throws { try Validate.notNull(obj: html) try Validate.notNull(obj: parentNode) let context: Element? = parent() as? Element let nodes: Array = try Parser.parseFragment(html, context, getBaseUri()) try parentNode?.addChildren(index, nodes) } /** Wrap the supplied HTML around this node. @param html HTML to wrap around this element, e.g. {@code
}. Can be arbitrarily deep. @return this node, for chaining. */ @discardableResult open func wrap(_ html: String)throws->Node? { try Validate.notEmpty(string: html) let context: Element? = parent() as? Element var wrapChildren: Array = try Parser.parseFragment(html, context, getBaseUri()) let wrapNode: Node? = wrapChildren.count > 0 ? wrapChildren[0] : nil if (wrapNode == nil || !(((wrapNode as? Element) != nil))) { // nothing to wrap with; noop return nil } let wrap: Element = wrapNode as! Element let deepest: Element = getDeepChild(el: wrap) try parentNode?.replaceChild(self, wrap) wrapChildren = wrapChildren.filter { $0 != wrap} try deepest.addChildren(self) // remainder (unbalanced wrap, like

-- The

is remainder if (wrapChildren.count > 0) { for i in 0.. * For example, with the input html: *

*

{@code

One Two Three
}

* Calling {@code element.unwrap()} on the {@code span} element will result in the html: *

{@code

One Two Three
}

* and the {@code "Two "} {@link TextNode} being returned. * * @return the first child of this node, after the node has been unwrapped. Null if the node had no children. * @see #remove() * @see #wrap(String) */ @discardableResult open func unwrap()throws ->Node? { try Validate.notNull(obj: parentNode) let firstChild: Node? = childNodes.count > 0 ? childNodes[0] : nil try parentNode?.addChildren(siblingIndex, self.childNodesAsArray()) try self.remove() return firstChild } private func getDeepChild(el: Element) -> Element { let children = el.children() if (children.size() > 0) { return getDeepChild(el: children.get(0)) } else { return el } } /** * Replace this node in the DOM with the supplied node. * @param in the node that will will replace the existing node. */ public func replaceWith(_ input: Node)throws { try Validate.notNull(obj: input) try Validate.notNull(obj: parentNode) try parentNode?.replaceChild(self, input) } public func setParentNode(_ parentNode: Node)throws { if (self.parentNode != nil) { try self.parentNode?.removeChild(self) } self.parentNode = parentNode } public func replaceChild(_ out: Node, _ input: Node)throws { try Validate.isTrue(val: out.parentNode === self) try Validate.notNull(obj: input) if (input.parentNode != nil) { try input.parentNode?.removeChild(input) } let index: Int = out.siblingIndex childNodes[index] = input input.parentNode = self input.setSiblingIndex(index) out.parentNode = nil } public func removeChild(_ out: Node)throws { try Validate.isTrue(val: out.parentNode === self) let index: Int = out.siblingIndex childNodes.remove(at: index) reindexChildren(index) out.parentNode = nil } public func addChildren(_ children: Node...)throws { //most used. short circuit addChildren(int), which hits reindex children and array copy try addChildren(children) } public func addChildren(_ children: [Node])throws { //most used. short circuit addChildren(int), which hits reindex children and array copy for child in children { try reparentChild(child) ensureChildNodes() childNodes.append(child) child.setSiblingIndex(childNodes.count-1) } } public func addChildren(_ index: Int, _ children: Node...)throws { try addChildren(index, children) } public func addChildren(_ index: Int, _ children: [Node])throws { ensureChildNodes() for i in (0..() // } } public func reparentChild(_ child: Node)throws { if (child.parentNode != nil) { try child.parentNode?.removeChild(child) } try child.setParentNode(self) } private func reindexChildren(_ start: Int) { for i in start..Array { if (parentNode == nil) { return Array() } let nodes: Array = parentNode!.childNodes var siblings: Array = Array() for node in nodes { if (node !== self) { siblings.append(node) } } return siblings } /** Get this node's next sibling. @return next sibling, or null if this is the last sibling */ open func nextSibling() -> Node? { guard let siblings: Array = parentNode?.childNodes else { return nil } let index: Int = siblingIndex+1 if (siblings.count > index) { return siblings[index] } else { return nil } } /** Get this node's previous sibling. @return the previous sibling, or null if this is the first sibling */ open func previousSibling() -> Node? { if (parentNode == nil) { return nil // root } if (siblingIndex > 0) { return parentNode?.childNodes[siblingIndex-1] } else { return nil } } public func setSiblingIndex(_ siblingIndex: Int) { self.siblingIndex = siblingIndex } /** * Perform a depth-first traversal through this node and its descendants. * @param nodeVisitor the visitor callbacks to perform on each node * @return this node, for chaining */ @discardableResult open func traverse(_ nodeVisitor: NodeVisitor)throws->Node { let traversor: NodeTraversor = NodeTraversor(nodeVisitor) try traversor.traverse(self) return self } /** Get the outer HTML of this node. @return HTML */ open func outerHtml()throws->String { let accum: StringBuilder = StringBuilder(128) try outerHtml(accum) return accum.toString() } public func outerHtml(_ accum: StringBuilder)throws { try NodeTraversor(OuterHtmlVisitor(accum, getOutputSettings())).traverse(self) } // if this node has no document (or parent), retrieve the default output settings func getOutputSettings() -> OutputSettings { return ownerDocument() != nil ? ownerDocument()!.outputSettings() : (Document(Node.empty)).outputSettings() } /** Get the outer HTML of this node. @param accum accumulator to place HTML into @throws IOException if appending to the given accumulator fails. */ func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) throws { preconditionFailure("This method must be overridden") } func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) throws { preconditionFailure("This method must be overridden") } /** * Write this node and its children to the given {@link Appendable}. * * @param appendable the {@link Appendable} to write to. * @return the supplied {@link Appendable}, for chaining. */ open func html(_ appendable: StringBuilder)throws -> StringBuilder { try outerHtml(appendable) return appendable } public func indent(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) { accum.append(UnicodeScalar.BackslashN).append(StringUtil.padding(depth * Int(out.indentAmount()))) } /** * Check if this node is the same instance of another (object identity test). * @param o other object to compare to * @return true if the content of this node is the same as the other * @see Node#hasSameValue(Object) to compare nodes by their value */ open func equals(_ o: Node) -> Bool { // implemented just so that javadoc is clear this is an identity test return self === o } /** * Check if this node is has the same content as another node. A node is considered the same if its name, attributes and content match the * other node; particularly its position in the tree does not influence its similarity. * @param o other object to compare to * @return true if the content of this node is the same as the other */ open func hasSameValue(_ o: Node)throws->Bool { if (self === o) {return true} // if (type(of:self) != type(of: o)) // { // return false // } return try self.outerHtml() == o.outerHtml() } /** * Create a stand-alone, deep copy of this node, and all of its children. The cloned node will have no siblings or * parent node. As a stand-alone object, any changes made to the clone or any of its children will not impact the * original node. *

* The cloned node may be adopted into another Document or node structure using {@link Element#appendChild(Node)}. * @return stand-alone cloned node */ public func copy(with zone: NSZone? = nil) -> Any { return copy(clone: Node()) } public func copy(parent: Node?) -> Node { let clone = Node() return copy(clone: clone, parent: parent) } public func copy(clone: Node) -> Node { let thisClone: Node = copy(clone: clone, parent: nil) // splits for orphan // Queue up nodes that need their children cloned (BFS). var nodesToProcess: Array = Array() nodesToProcess.append(thisClone) while (!nodesToProcess.isEmpty) { let currParent: Node = nodesToProcess.removeFirst() for i in 0.. Node { clone.parentNode = parent // can be null, to create an orphan split clone.siblingIndex = parent == nil ? 0 : siblingIndex clone.attributes = attributes != nil ? attributes?.clone() : nil clone.baseUri = baseUri clone.childNodes = Array() for child in childNodes { clone.childNodes.append(child) } return clone } private class OuterHtmlVisitor: NodeVisitor { private var accum: StringBuilder private var out: OutputSettings static private let text = "#text" init(_ accum: StringBuilder, _ out: OutputSettings) { self.accum = accum self.out = out } open func head(_ node: Node, _ depth: Int)throws { try node.outerHtmlHead(accum, depth, out) } open func tail(_ node: Node, _ depth: Int)throws { // When compiling a release optimized swift linux 4.2 version the "saves a void hit." // causes a SIL error. Removing optimization on linux until a fix is found. #if os(Linux) try node.outerHtmlTail(accum, depth, out) #else if (!(node.nodeName() == OuterHtmlVisitor.text)) { // saves a void hit. try node.outerHtmlTail(accum, depth, out) } #endif } } /// Returns a Boolean value indicating whether two values are equal. /// /// Equality is the inverse of inequality. For any values `a` and `b`, /// `a == b` implies that `a != b` is `false`. /// /// - Parameters: /// - lhs: A value to compare. /// - rhs: Another value to compare. public static func ==(lhs: Node, rhs: Node) -> Bool { return lhs === rhs } /// The hash value. /// /// Hash values are not guaranteed to be equal across different executions of /// your program. Do not save hash values to use during a future execution. public func hash(into hasher: inout Hasher) { hasher.combine(description) hasher.combine(baseUri) } } extension Node: CustomStringConvertible { public var description: String { do { return try outerHtml() } catch { } return Node.empty } } extension Node: CustomDebugStringConvertible { private static let space = " " public var debugDescription: String { do { return try String(describing: type(of: self)) + Node.space + outerHtml() } catch { } return String(describing: type(of: self)) } }