+ * Attributes are treated as a map: there can be only one value associated with an attribute key/name. + *
+ *+ * Attribute name and value comparisons are case sensitive. By default for HTML, attribute names are + * normalized to lower-case on parsing. That means you should use lower-case strings when referring to attributes by + * name. + *
+ * + * + */ +open class Attributes: NSCopying { + + public static var dataPrefix: String = "data-" + + // Stored by lowercased key, but key case is checked against the copy inside + // the Attribute on retrieval. + var attributes: [Attribute] = [] + + public init() {} + + /** + Get an attribute value by key. + @param key the (case-sensitive) attribute key + @return the attribute value if set; or empty string if not set. + @see #hasKey(String) + */ + open func get(key: String) -> String { + if let attr = attributes.first(where: { $0.getKey() == key }) { + return attr.getValue() + } + return "" + } + + /** + * Get an attribute's value by case-insensitive key + * @param key the attribute name + * @return the first matching attribute value if set; or empty string if not set. + */ + open func getIgnoreCase(key: String )throws -> String { + try Validate.notEmpty(string: key) + if let attr = attributes.first(where: { $0.getKey().caseInsensitiveCompare(key) == .orderedSame }) { + return attr.getValue() + } + return "" + } + + /** + Set a new attribute, or replace an existing one by key. + @param key attribute key + @param value attribute value + */ + open func put(_ key: String, _ value: String) throws { + let attr = try Attribute(key: key, value: value) + put(attribute: attr) + } + + /** + Set a new boolean attribute, remove attribute if value is false. + @param key attribute key + @param value attribute value + */ + open func put(_ key: String, _ value: Bool) throws { + if (value) { + try put(attribute: BooleanAttribute(key: key)) + } else { + try remove(key: key) + } + } + + /** + Set a new attribute, or replace an existing one by (case-sensitive) key. + @param attribute attribute + */ + open func put(attribute: Attribute) { + let key = attribute.getKey() + if let ix = attributes.firstIndex(where: { $0.getKey() == key }) { + attributes[ix] = attribute + } else { + attributes.append(attribute) + } + } + + /** + Remove an attribute by key. Case sensitive. + @param key attribute key to remove + */ + open func remove(key: String)throws { + try Validate.notEmpty(string: key) + if let ix = attributes.firstIndex(where: { $0.getKey() == key }) { + attributes.remove(at: ix) } + } + + /** + Remove an attribute by key. Case insensitive. + @param key attribute key to remove + */ + open func removeIgnoreCase(key: String ) throws { + try Validate.notEmpty(string: key) + if let ix = attributes.firstIndex(where: { $0.getKey().caseInsensitiveCompare(key) == .orderedSame}) { + attributes.remove(at: ix) + } + } + + /** + Tests if these attributes contain an attribute with this key. + @param key case-sensitive key to check for + @return true if key exists, false otherwise + */ + open func hasKey(key: String) -> Bool { + return attributes.contains(where: { $0.getKey() == key }) + } + + /** + Tests if these attributes contain an attribute with this key. + @param key key to check for + @return true if key exists, false otherwise + */ + open func hasKeyIgnoreCase(key: String) -> Bool { + return attributes.contains(where: { $0.getKey().caseInsensitiveCompare(key) == .orderedSame}) + } + + /** + Get the number of attributes in this set. + @return size + */ + open func size() -> Int { + return attributes.count + } + + /** + Add all the attributes from the incoming set to this set. + @param incoming attributes to add to these attributes. + */ + open func addAll(incoming: Attributes?) { + guard let incoming = incoming else { return } + for attr in incoming.attributes { + put(attribute: attr) + } + } + + /** + Get the attributes as a List, for iteration. Do not modify the keys of the attributes via this view, as changes + to keys will not be recognised in the containing set. + @return an view of the attributes as a List. + */ + open func asList() -> [Attribute] { + return attributes + } + + /** + * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys + * starting with {@code data-}. + * @return map of custom data attributes. + */ + open func dataset() -> [String: String] { + let prefixLength = Attributes.dataPrefix.count + let pairs = attributes.filter { $0.isDataAttribute() } + .map { ($0.getKey().substring(prefixLength), $0.getValue()) } + return Dictionary(uniqueKeysWithValues: pairs) + } + + /** + Get the HTML representation of these attributes. + @return HTML + @throws SerializationException if the HTML representation of the attributes cannot be constructed. + */ + open func html()throws -> String { + let accum = StringBuilder() + try html(accum: accum, out: Document("").outputSettings()) // output settings a bit funky, but this html() seldom used + return accum.toString() + } + + public func html(accum: StringBuilder, out: OutputSettings ) throws { + for attr in attributes { + accum.append(" ") + attr.html(accum: accum, out: out) + } + } + + open func toString()throws -> String { + return try html() + } + + /** + * Checks if these attributes are equal to another set of attributes, by comparing the two sets + * @param o attributes to compare with + * @return if both sets of attributes have the same content + */ + open func equals(o: AnyObject?) -> Bool { + if(o == nil) {return false} + if (self === o.self) {return true} + guard let that = o as? Attributes else {return false} + return (attributes == that.attributes) + } + + open func lowercaseAllKeys() { + for ix in attributes.indices { + attributes[ix].key = attributes[ix].key.lowercased() + } + } + + public func copy(with zone: NSZone? = nil) -> Any { + let clone = Attributes() + clone.attributes = attributes + return clone + } + + open func clone() -> Attributes { + return self.copy() as! Attributes + } + + fileprivate static func dataKey(key: String) -> String { + return dataPrefix + key + } + +} + +extension Attributes: Sequence { + public func makeIterator() -> AnyIterator+ * A selector is a chain of simple selectors, separated by combinators. Selectors are case insensitive (including against + * elements, attributes, and attribute values). + *
+ *+ * The universal selector (*) is implicit when no element selector is supplied (i.e. {@code *.header} and {@code .header} + * is equivalent). + *
+ *Pattern | Matches | Example | |
---|---|---|---|
* | any element | * | |
tag | elements with the given tag name | div | |
*|E | elements of type E in any namespace ns | *|name finds <fb:name> elements | |
ns|E | elements of type E in the namespace ns | fb|name finds <fb:name> elements | |
#id | elements with attribute ID of "id" | div#wrap , #logo | |
.class | elements with a class name of "class" | div.left , .result | |
[attr] | elements with an attribute named "attr" (with any value) | a[href] , [title] | |
[^attrPrefix] | elements with an attribute name starting with "attrPrefix". Use to find elements with HTML5 datasets | [^data-] , div[^data-] | |
[attr=val] | elements with an attribute named "attr", and value equal to "val" | img[width=500] , a[rel=nofollow] | |
[attr="val"] | elements with an attribute named "attr", and value equal to "val" | span[hello="Cleveland"][goodbye="Columbus"] , a[rel="nofollow"] | |
[attr^=valPrefix] | elements with an attribute named "attr", and value starting with "valPrefix" | a[href^=http:] | |
[attr$=valSuffix] | elements with an attribute named "attr", and value ending with "valSuffix" | img[src$=.png] | |
[attr*=valContaining] | elements with an attribute named "attr", and value containing "valContaining" | a[href*=/search/] | |
[attr~=regex] | elements with an attribute named "attr", and value matching the regular expression | img[src~=(?i)\\.(png|jpe?g)] | |
The above may be combined in any order | div.header[title] | ||
Combinators | |||
E F | an F element descended from an E element | div a , .logo h1 | |
E {@literal >} F | an F direct child of E | ol {@literal >} li | |
E + F | an F element immediately preceded by sibling E | li + li , div.head + div | |
E ~ F | an F element preceded by sibling E | h1 ~ p | |
E, F, G | all matching elements E, F, or G | a[href], div, h3 | |
Pseudo selectors | |||
:lt(n) | elements whose sibling index is less than n | td:lt(3) finds the first 3 cells of each row | |
:gt(n) | elements whose sibling index is greater than n | td:gt(1) finds cells after skipping the first two | |
:eq(n) | elements whose sibling index is equal to n | td:eq(0) finds the first cell of each row | |
:has(selector) | elements that contains at least one element matching the selector | div:has(p) finds divs that contain p elements | |
:not(selector) | elements that do not match the selector. See also {@link Elements#not(String)} | div:not(.logo) finds all divs that do not have the "logo" class.
| |
:contains(text) | elements that contains the specified text. The search is case insensitive. The text may appear in the found element, or any of its descendants. | p:contains(SwiftSoup) finds p elements containing the text "SwiftSoup". | |
:matches(regex) | elements whose text matches the specified regular expression. The text may appear in the found element, or any of its descendants. | td:matches(\\d+) finds table cells containing digits. div:matches((?i)login) finds divs containing the text, case insensitively. | |
:containsOwn(text) | elements that directly contain the specified text. The search is case insensitive. The text must appear in the found element, not any of its descendants. | p:containsOwn(SwiftSoup) finds p elements with own text "SwiftSoup". | |
:matchesOwn(regex) | elements whose own text matches the specified regular expression. The text must appear in the found element, not any of its descendants. | td:matchesOwn(\\d+) finds table cells directly containing digits. div:matchesOwn((?i)login) finds divs containing the text, case insensitively. | |
The above may be combined in any order and with other selectors | .light:contains(name):eq(0) | ||
Structural pseudo selectors | |||
:root | The element that is the root of the document. In HTML, this is the html element | :root | |
:nth-child(an+b) | elements that have :nth-child() can take odd and even as arguments instead. odd has the same signification as 2n+1 , and even has the same signification as 2n . | tr:nth-child(2n+1) finds every odd row of a table. :nth-child(10n-1) the 9th, 19th, 29th, etc, element. li:nth-child(5) the 5h li | |
:nth-last-child(an+b) | elements that have an+b-1 siblings after it in the document tree. Otherwise like :nth-child() | tr:nth-last-child(-n+2) the last two rows of a table | |
:nth-of-type(an+b) | pseudo-class notation represents an element that has an+b-1 siblings with the same expanded element name before it in the document tree, for any zero or positive integer value of n, and has a parent element | img:nth-of-type(2n+1) | |
:nth-last-of-type(an+b) | pseudo-class notation represents an element that has an+b-1 siblings with the same expanded element name after it in the document tree, for any zero or positive integer value of n, and has a parent element | img:nth-last-of-type(2n+1) | |
:first-child | elements that are the first child of some other element. | div {@literal >} p:first-child | |
:last-child | elements that are the last child of some other element. | ol {@literal >} li:last-child | |
:first-of-type | elements that are the first sibling of its type in the list of children of its parent element | dl dt:first-of-type | |
:last-of-type | elements that are the last sibling of its type in the list of children of its parent element | tr {@literal >} td:last-of-type | |
:only-child | elements that have a parent element and whose parent element hasve no other element children | ||
:only-of-type | an element that has a parent element and whose parent element has no other element children with the same expanded element name | ||
:empty | elements that have no children at all |
This enables + * {@link #updateMetaCharsetElement(boolean) meta charset update}.
+ * + *If there's no element with charset / encoding information yet it will + * be created. Obsolete charset / encoding definitions are removed!
+ * + *Elements used:
+ * + *If set to false (default) there are no elements + * modified.
+ * + * @param update If true the element updated on charset + * changes, false if not + * + * @see #charset(java.nio.charset.Charset) + */ + public func updateMetaCharsetElement(_ update: Bool) { + self.updateMetaCharset = update + } + + /** + * Returns whether the element with charset information in this document is + * updated on changes through {@link #charset(java.nio.charset.Charset) + * Document.charset(Charset)} or not. + * + * @return Returns true if the element is updated on charset + * changes, false if not + */ + public func updateMetaCharsetElement() -> Bool { + return updateMetaCharset + } + + /** + * Ensures a meta charset (html) or xml declaration (xml) with the current + * encoding used. This only applies with + * {@link #updateMetaCharsetElement(boolean) updateMetaCharset} set to + * true, otherwise this method does nothing. + * + *Elements used:
+ * + *base
, which provides a limited set of named HTML
+ * entities and escapes other characters as numbered entities for maximum compatibility; or extended
,
+ * which uses the complete set of HTML named entities.
+ *
+ * The default escape mode is base
.
+ * @return the document's current escape mode
+ */
+ public func escapeMode() -> Entities.EscapeMode {
+ return _escapeMode
+ }
+
+ /**
+ * Set the document's escape mode, which determines how characters are escaped when the output character set
+ * does not support a given character:- using either a named or a numbered escape.
+ * @param escapeMode the new escape mode to use
+ * @return the document's output settings, for chaining
+ */
+ @discardableResult
+ public func escapeMode(_ escapeMode: Entities.EscapeMode) -> OutputSettings {
+ self._escapeMode = escapeMode
+ return self
+ }
+
+ /**
+ * Get the document's current output charset, which is used to control which characters are escaped when
+ * generating HTML (via the html()
methods), and which are kept intact.
+ *
+ * Where possible (when parsing from a URL or File), the document's output charset is automatically set to the
+ * input charset. Otherwise, it defaults to UTF-8.
+ * @return the document's current charset.
+ */
+ public func encoder() -> String.Encoding {
+ return _encoder
+ }
+ public func charset() -> String.Encoding {
+ return _encoder
+ }
+
+ /**
+ * Update the document's output charset.
+ * @param charset the new charset to use.
+ * @return the document's output settings, for chaining
+ */
+ @discardableResult
+ public func encoder(_ encoder: String.Encoding) -> OutputSettings {
+ self._encoder = encoder
+ return self
+ }
+
+ @discardableResult
+ public func charset(_ e: String.Encoding) -> OutputSettings {
+ return encoder(e)
+ }
+
+ /**
+ * Get the document's current output syntax.
+ * @return current syntax
+ */
+ public func syntax() -> Syntax {
+ return _syntax
+ }
+
+ /**
+ * Set the document's output syntax. Either {@code html}, with empty tags and boolean attributes (etc), or
+ * {@code xml}, with self-closing tags.
+ * @param syntax serialization syntax
+ * @return the document's output settings, for chaining
+ */
+ @discardableResult
+ public func syntax(syntax: Syntax) -> OutputSettings {
+ _syntax = syntax
+ return self
+ }
+
+ /**
+ * Get if pretty printing is enabled. Default is true. If disabled, the HTML output methods will not re-format
+ * the output, and the output will generally look like the input.
+ * @return if pretty printing is enabled.
+ */
+ public func prettyPrint() -> Bool {
+ return _prettyPrint
+ }
+
+ /**
+ * Enable or disable pretty printing.
+ * @param pretty new pretty print setting
+ * @return this, for chaining
+ */
+ @discardableResult
+ public func prettyPrint(pretty: Bool) -> OutputSettings {
+ _prettyPrint = pretty
+ return self
+ }
+
+ /**
+ * Get if outline mode is enabled. Default is false. If enabled, the HTML output methods will consider
+ * all tags as block.
+ * @return if outline mode is enabled.
+ */
+ public func outline() -> Bool {
+ return _outline
+ }
+
+ /**
+ * Enable or disable HTML outline mode.
+ * @param outlineMode new outline setting
+ * @return this, for chaining
+ */
+ @discardableResult
+ public func outline(outlineMode: Bool) -> OutputSettings {
+ _outline = outlineMode
+ return self
+ }
+
+ /**
+ * Get the current tag indent amount, used when pretty printing.
+ * @return the current indent amount
+ */
+ public func indentAmount() -> UInt {
+ return _indentAmount
+ }
+
+ /**
+ * Set the indent amount for pretty printing
+ * @param indentAmount number of spaces to use for indenting each level. Must be {@literal >=} 0.
+ * @return this, for chaining
+ */
+ @discardableResult
+ public func indentAmount(indentAmount: UInt) -> OutputSettings {
+ _indentAmount = indentAmount
+ return self
+ }
+
+ public func copy(with zone: NSZone? = nil) -> Any {
+ let clone: OutputSettings = OutputSettings()
+ clone.charset(_encoder) // new charset and charset encoder
+ clone._escapeMode = _escapeMode//Entities.EscapeMode.valueOf(escapeMode.name())
+ // indentAmount, prettyPrint are primitives so object.clone() will handle
+ return clone
+ }
+
+}
diff --git a/Swiftgram/SwiftSoup/Sources/DocumentType.swift b/Swiftgram/SwiftSoup/Sources/DocumentType.swift
new file mode 100644
index 0000000000..95f9b10df3
--- /dev/null
+++ b/Swiftgram/SwiftSoup/Sources/DocumentType.swift
@@ -0,0 +1,129 @@
+//
+// DocumentType.swift
+// SwifSoup
+//
+// Created by Nabil Chatbi on 29/09/16.
+// Copyright © 2016 Nabil Chatbi.. All rights reserved.
+//
+
+import Foundation
+
+/**
+ * A {@code } node.
+ */
+public class DocumentType: Node {
+ static let PUBLIC_KEY: String = "PUBLIC"
+ static let SYSTEM_KEY: String = "SYSTEM"
+ private static let NAME: String = "name"
+ private static let PUB_SYS_KEY: String = "pubSysKey"; // PUBLIC or SYSTEM
+ private static let PUBLIC_ID: String = "publicId"
+ private static let SYSTEM_ID: String = "systemId"
+ // todo: quirk mode from publicId and systemId
+
+ /**
+ * Create a new doctype element.
+ * @param name the doctype's name
+ * @param publicId the doctype's public ID
+ * @param systemId the doctype's system ID
+ * @param baseUri the doctype's base URI
+ */
+ public init(_ name: String, _ publicId: String, _ systemId: String, _ baseUri: String) {
+ super.init(baseUri)
+ do {
+ try attr(DocumentType.NAME, name)
+ try attr(DocumentType.PUBLIC_ID, publicId)
+ if (has(DocumentType.PUBLIC_ID)) {
+ try attr(DocumentType.PUB_SYS_KEY, DocumentType.PUBLIC_KEY)
+ }
+ try attr(DocumentType.SYSTEM_ID, systemId)
+ } catch {}
+ }
+
+ /**
+ * Create a new doctype element.
+ * @param name the doctype's name
+ * @param publicId the doctype's public ID
+ * @param systemId the doctype's system ID
+ * @param baseUri the doctype's base URI
+ */
+ public init(_ name: String, _ pubSysKey: String?, _ publicId: String, _ systemId: String, _ baseUri: String) {
+ super.init(baseUri)
+ do {
+ try attr(DocumentType.NAME, name)
+ if(pubSysKey != nil) {
+ try attr(DocumentType.PUB_SYS_KEY, pubSysKey!)
+ }
+ try attr(DocumentType.PUBLIC_ID, publicId)
+ try attr(DocumentType.SYSTEM_ID, systemId)
+ } catch {}
+ }
+
+ public override func nodeName() -> String {
+ return "#doctype"
+ }
+
+ override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {
+ if (out.syntax() == OutputSettings.Syntax.html && !has(DocumentType.PUBLIC_ID) && !has(DocumentType.SYSTEM_ID)) {
+ // looks like a html5 doctype, go lowercase for aesthetics
+ accum.append("")
+ }
+
+ override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {
+ }
+
+ private func has(_ attribute: String) -> Bool {
+ do {
+ return !StringUtil.isBlank(try attr(attribute))
+ } catch {return false}
+ }
+
+ public override func copy(with zone: NSZone? = nil) -> Any {
+ let clone = DocumentType(attributes!.get(key: DocumentType.NAME),
+ attributes!.get(key: DocumentType.PUBLIC_ID),
+ attributes!.get(key: DocumentType.SYSTEM_ID),
+ baseUri!)
+ return copy(clone: clone)
+ }
+
+ public override func copy(parent: Node?) -> Node {
+ let clone = DocumentType(attributes!.get(key: DocumentType.NAME),
+ attributes!.get(key: DocumentType.PUBLIC_ID),
+ attributes!.get(key: DocumentType.SYSTEM_ID),
+ baseUri!)
+ return copy(clone: clone, parent: parent)
+ }
+
+ public override func copy(clone: Node, parent: Node?) -> Node {
+ return super.copy(clone: clone, parent: parent)
+ }
+
+}
diff --git a/Swiftgram/SwiftSoup/Sources/Element.swift b/Swiftgram/SwiftSoup/Sources/Element.swift
new file mode 100644
index 0000000000..630b9914bc
--- /dev/null
+++ b/Swiftgram/SwiftSoup/Sources/Element.swift
@@ -0,0 +1,1316 @@
+//
+// Element.swift
+// SwifSoup
+//
+// Created by Nabil Chatbi on 29/09/16.
+// Copyright © 2016 Nabil Chatbi.. All rights reserved.
+//
+
+import Foundation
+
+open class Element: Node {
+ var _tag: Tag
+
+ private static let classString = "class"
+ private static let emptyString = ""
+ private static let idString = "id"
+ private static let rootString = "#root"
+
+ //private static let classSplit : Pattern = Pattern("\\s+")
+ private static let classSplit = "\\s+"
+
+ /**
+ * Create a new, standalone Element. (Standalone in that is has no parent.)
+ *
+ * @param tag tag of this element
+ * @param baseUri the base URI
+ * @param attributes initial attributes
+ * @see #appendChild(Node)
+ * @see #appendElement(String)
+ */
+ public init(_ tag: Tag, _ baseUri: String, _ attributes: Attributes) {
+ self._tag = tag
+ super.init(baseUri, attributes)
+ }
+ /**
+ * Create a new Element from a tag and a base URI.
+ *
+ * @param tag element tag
+ * @param baseUri the base URI of this element. It is acceptable for the base URI to be an empty
+ * string, but not null.
+ * @see Tag#valueOf(String, ParseSettings)
+ */
+ public init(_ tag: Tag, _ baseUri: String) {
+ self._tag = tag
+ super.init(baseUri, Attributes())
+ }
+
+ open override func nodeName() -> String {
+ return _tag.getName()
+ }
+ /**
+ * Get the name of the tag for this element. E.g. {@code div}
+ *
+ * @return the tag name
+ */
+ open func tagName() -> String {
+ return _tag.getName()
+ }
+ open func tagNameNormal() -> String {
+ return _tag.getNameNormal()
+ }
+
+ /**
+ * Change the tag of this element. For example, convert a {@code } to a {@code == false}).
+ *
+ * @return true if block, false if not (and thus inline)
+ */
+ open func isBlock() -> Bool {
+ return _tag.isBlock()
+ }
+
+ /**
+ * Get the {@code id} attribute of this element.
+ *
+ * @return The id attribute, if present, or an empty string if not.
+ */
+ open func id() -> String {
+ guard let attributes = attributes else {return Element.emptyString}
+ do {
+ return try attributes.getIgnoreCase(key: Element.idString)
+ } catch {}
+ return Element.emptyString
+ }
+
+ /**
+ * Set an attribute value on this element. If this element already has an attribute with the
+ * key, its value is updated; otherwise, a new attribute is added.
+ *
+ * @return this element
+ */
+ @discardableResult
+ open override func attr(_ attributeKey: String, _ attributeValue: String)throws->Element {
+ try super.attr(attributeKey, attributeValue)
+ return self
+ }
+
+ /**
+ * Set a boolean attribute value on this element. Setting to
+ * E.g., the element {@code
+ * This map is a filtered view of the element's attribute map. Changes to one map (add, remove, update) are reflected
+ * in the other map.
+ *
+ * You can find elements that have data attributes using the {@code [^data-]} attribute key prefix selector.
+ * @return a map of {@code key=value} custom data attributes.
+ */
+ open func dataset()->Dictionary
+ * Note that an element can have both mixed Nodes and Elements as children. This method inspects
+ * a filtered list of children that are elements, and the index is based on that filtered list.
+ *
+ * This is effectively a filter on {@link #childNodes()} to get Element nodes.
+ *
+ * This is effectively a filter on {@link #childNodes()} to get Text nodes.
+ * @return child text nodes. If this element has no text nodes, returns an
+ * empty list.
+ * One Two Three
+ * This is effectively a filter on {@link #childNodes()} to get Data nodes.
+ *
+ * This method is generally more powerful to use than the DOM-type {@code getElementBy*} methods, because
+ * multiple filters can be combined, e.g.:
+ *
+ * See the query syntax documentation in {@link CssSelector}.
+ *
+ * If the element has an ID, returns #id;
+ * otherwise returns the parent (if any) CSS selector, followed by {@literal '>'},
+ * followed by a unique selector for the element (tag.class.class:nth-child(n)).
+ *
+ * This is similar to {@link #nextSibling()}, but specifically finds only Elements
+ *
+ * Note that this finds the first matching ID, starting with this element. If you search down from a different
+ * starting point, it is possible to find a different element by ID. For unique element by ID within a Document,
+ * use {@link Document#getElementById(String)}
+ * @param id The ID to search for.
+ * @return The first matching element by ID, starting with this element, or null if none found.
+ */
+ public func getElementById(_ id: String)throws->Element? {
+ try Validate.notEmpty(string: id)
+
+ let elements: Elements = try Collector.collect(Evaluator.Id(id), self)
+ if (elements.array().count > 0) {
+ return elements.get(0)
+ } else {
+ return nil
+ }
+ }
+
+ /**
+ * Find elements that have this class, including or under this element. Case insensitive.
+ *
+ * Elements can have multiple classes (e.g. {@code
+ * For example, given HTML {@code Hello there now!
+ * For example, given HTML {@code Hello there now! }, would return
+ * {@code
+To get an {@code Elements} object, use the {@link Element#select(String)} method.
+
+ * Note that it is possible to get repeats if the matched elements contain both parent elements and their own
+ * children, as the Element.text() method returns the combined text of a parent and all its children.
+ * @return string of all text: unescaped and no HTML.
+ * @see Element#text()
+ */
+ open func text(trimAndNormaliseWhitespace: Bool = true)throws->String {
+ let sb: StringBuilder = StringBuilder()
+ for element: Element in this {
+ if !sb.isEmpty {
+ sb.append(" ")
+ }
+ sb.append(try element.text(trimAndNormaliseWhitespace: trimAndNormaliseWhitespace))
+ }
+ return sb.toString()
+ }
+
+ /// Check if an element has text
+ open func hasText() -> Bool {
+ for element: Element in this {
+ if (element.hasText()) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * Get the text content of each of the matched elements. If an element has no text, then it is not included in the
+ * result.
+ * @return A list of each matched element's text content.
+ * @see Element#text()
+ * @see Element#hasText()
+ * @see #text()
+ */
+ public func eachText()throws->Array This is SwiftSoup This is SwiftSoup
+ * This is useful for e.g removing unwanted formatting elements but keeping their contents.
+ * {@code {@code doc.select("font").unwrap();} HTML = {@code
+ * E.g. HTML: {@code Hello there now
+ * E.g. HTML: {@code Hello there
+ * Note that this method should not be used to clean user-submitted HTML; rather, use {@link Cleaner} to clean HTML.
+ * @return this, for chaining
+ * @see Element#empty()
+ * @see #empty()
+ */
+ @discardableResult
+ open func remove()throws->Elements {
+ for element in this {
+ try element.remove()
+ }
+ return self
+ }
+
+ // filters
+
+ /**
+ * Find matching elements within this element list.
+ * @param query A {@link CssSelector} query
+ * @return the filtered list of elements, or an empty list if none match.
+ */
+ open func select(_ query: String)throws->Elements {
+ return try CssSelector.select(query, this)
+ }
+
+ /**
+ * Remove elements from this list that match the {@link CssSelector} query.
+ *
+ * E.g. HTML: {@code
+ * @param query the selector query whose results should be removed from these elements
+ * @return a new elements list that contains only the filtered results
+ */
+ open func not(_ query: String)throws->Elements {
+ let out: Elements = try CssSelector.select(query, this)
+ return CssSelector.filterOut(this, out.this)
+ }
+
+ /**
+ * Get the nth matched element as an Elements object.
+ *
+ * See also {@link #get(int)} to retrieve an Element.
+ * @param index the (zero-based) index of the element in the list to retain
+ * @return Elements containing only the specified element, or, if that element did not exist, an empty list.
+ */
+ open func eq(_ index: Int) -> Elements {
+ return size() > index ? Elements([get(index)]) : Elements()
+ }
+
+ /**
+ * Test if any of the matched elements match the supplied query.
+ * @param query A selector
+ * @return true if at least one element in the list matches the query.
+ */
+ open func iS(_ query: String)throws->Bool {
+ let eval: Evaluator = try QueryParser.parse(query)
+ for e: Element in this {
+ if (try e.iS(eval)) {
+ return true
+ }
+ }
+ return false
+
+ }
+
+ /**
+ * Get all of the parents and ancestor elements of the matched elements.
+ * @return all of the parents and ancestor elements of the matched elements
+ */
+
+ open func parents() -> Elements {
+ let combo: OrderedSet
+ * To get an absolute URL from an attribute that may be a relative URL, prefix the key with
+ * E.g.:
+ * If the attribute value is already absolute (i.e. it starts with a protocol, like
+ *
+ * As an alternate, you can use the {@link #attr} method with the is remainder
+ if (wrapChildren.count > 0) {
+ for i in 0.. {@code {@code
+ * 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
+ * This interface provides two methods, {@code head} and {@code tail}. The head method is called when the node is first
+ * seen, and the tail method when all of the node's children have been visited. As an example, head can be used to
+ * create a start tag for a node, and tail to create the end tag.
+ * true
sets the attribute value to "" and
+ * marks the attribute as boolean so no value is written out. Setting to false
removes the attribute
+ * with the same key if it exists.
+ *
+ * @param attributeKey the attribute key
+ * @param attributeValue the attribute value
+ *
+ * @return this element
+ */
+ @discardableResult
+ open func attr(_ attributeKey: String, _ attributeValue: Bool)throws->Element {
+ try attributes?.put(attributeKey, attributeValue)
+ return self
+ }
+
+ /**
+ * Get this element's HTML5 custom data attributes. Each attribute in the element that has a key
+ * starting with "data-" is included the dataset.
+ *
Four
+ *
+ */
+ open func textNodes()->Array
]}
, " Four"]}
+ *
+ * <div class="header gray">
returns, "header gray
")
+ * @return The literal class attribute, or empty string if no class attribute set.
+ */
+ public func className()throws->String {
+ return try attr(Element.classString).trim()
+ }
+
+ /**
+ * Get all of the element's class names. E.g. on element {@code in html,
in xml
+ }
+ } else {
+ accum.append(">")
+ }
+ }
+
+ override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {
+ if (!(childNodes.isEmpty && _tag.isSelfClosing())) {
+ if (out.prettyPrint() && (!childNodes.isEmpty && (
+ _tag.formatAsBlock() || (out.outline() && (childNodes.count>1 || (childNodes.count==1 && !(((childNodes[0] as? TextNode) != nil)))))
+ ))) {
+ indent(accum, depth, out)
+ }
+ accum.append("").append(tagName()).append(">")
+ }
+ }
+
+ /**
+ * Retrieves the element's inner HTML. E.g. on a {@code
doc.select("b").wrap("<i></i>");
+ becomes {@code
+ * doc.select("p").empty();
+ * HTML = {@code
+ * doc.select("p").remove();
+ * HTML = {@code
+ * Elements divs = doc.select("div").not(".logo");
+ * Result: {@code divs: [null
if contents is empty.
+ */
+ open func first() -> Element? {
+ return isEmpty() ? nil : get(0)
+ }
+
+ /// Check if no element stored
+ open func isEmpty() -> Bool {
+ return array().count == 0
+ }
+
+ /// Count
+ open func size() -> Int {
+ return array().count
+ }
+
+ /**
+ Get the last matched element.
+ @return The last matched element, or null
if contents is empty.
+ */
+ open func last() -> Element? {
+ return isEmpty() ? nil : get(size() - 1)
+ }
+
+ /**
+ * Perform a depth-first traversal on each of the selected elements.
+ * @param nodeVisitor the visitor callbacks to perform on each node
+ * @return this, for chaining
+ */
+ @discardableResult
+ open func traverse(_ nodeVisitor: NodeVisitor)throws->Elements {
+ let traversor: NodeTraversor = NodeTraversor(nodeVisitor)
+ for el: Element in this {
+ try traversor.traverse(el)
+ }
+ return self
+ }
+
+ /**
+ * Get the {@link FormElement} forms from the selected elements, if any.
+ * @return a list of {@link FormElement}s pulled from the matched elements. The list will be empty if the elements contain
+ * no forms.
+ */
+ open func forms()->Array, unless in svg
+ } else {
+ try tb.insert(startTag)
+ }
+ } else if (name.equals("isindex")) {
+ // how much do we care about the early 90s?
+ tb.error(self)
+ if (tb.getFormElement() != nil) {
+ return false
+ }
+
+ tb.tokeniser.acknowledgeSelfClosingFlag()
+ try tb.processStartTag("form")
+ if (startTag._attributes.hasKey(key: "action")) {
+ if let form: Element = tb.getFormElement() {
+ try form.attr("action", startTag._attributes.get(key: "action"))
+ }
+ }
+ try tb.processStartTag("hr")
+ try tb.processStartTag("label")
+ // hope you like english.
+ let prompt: String = startTag._attributes.hasKey(key: "prompt") ?
+ startTag._attributes.get(key: "prompt") :
+ "self is a searchable index. Enter search keywords: "
+
+ try tb.process(Token.Char().data(prompt))
+
+ // input
+ let inputAttribs: Attributes = Attributes()
+ for attr: Attribute in startTag._attributes {
+ if (!Constants.InBodyStartInputAttribs.contains(attr.getKey())) {
+ inputAttribs.put(attribute: attr)
+ }
+ }
+ try inputAttribs.put("name", "isindex")
+ try tb.processStartTag("input", inputAttribs)
+ try tb.processEndTag("label")
+ try tb.processStartTag("hr")
+ try tb.processEndTag("form")
+ } else if (name.equals("textarea")) {
+ try tb.insert(startTag)
+ // todo: If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move on to the next one. (Newlines at the start of textarea elements are ignored as an authoring convenience.)
+ tb.tokeniser.transition(TokeniserState.Rcdata)
+ tb.markInsertionMode()
+ tb.framesetOk(false)
+ tb.transition(.Text)
+ } else if (name.equals("xmp")) {
+ if (try tb.inButtonScope("p")) {
+ try tb.processEndTag("p")
+ }
+ try tb.reconstructFormattingElements()
+ tb.framesetOk(false)
+ try HtmlTreeBuilderState.handleRawtext(startTag, tb)
+ } else if (name.equals("iframe")) {
+ tb.framesetOk(false)
+ try HtmlTreeBuilderState.handleRawtext(startTag, tb)
+ } else if (name.equals("noembed")) {
+ // also handle noscript if script enabled
+ try HtmlTreeBuilderState.handleRawtext(startTag, tb)
+ } else if (name.equals("select")) {
+ try tb.reconstructFormattingElements()
+ try tb.insert(startTag)
+ tb.framesetOk(false)
+
+ let state: HtmlTreeBuilderState = tb.state()
+ if (state.equals(.InTable) || state.equals(.InCaption) || state.equals(.InTableBody) || state.equals(.InRow) || state.equals(.InCell)) {
+ tb.transition(.InSelectInTable)
+ } else {
+ tb.transition(.InSelect)
+ }
+ } else if Constants.InBodyStartOptions.contains(name) {
+ if (tb.currentElement() != nil && tb.currentElement()!.nodeName().equals("option")) {
+ try tb.processEndTag("option")
+ }
+ try tb.reconstructFormattingElements()
+ try tb.insert(startTag)
+ } else if Constants.InBodyStartRuby.contains(name) {
+ if (try tb.inScope("ruby")) {
+ tb.generateImpliedEndTags()
+ if (tb.currentElement() != nil && !tb.currentElement()!.nodeName().equals("ruby")) {
+ tb.error(self)
+ tb.popStackToBefore("ruby") // i.e. close up to but not include name
+ }
+ try tb.insert(startTag)
+ }
+ } else if (name.equals("math")) {
+ try tb.reconstructFormattingElements()
+ // todo: handle A start tag whose tag name is "math" (i.e. foreign, mathml)
+ try tb.insert(startTag)
+ tb.tokeniser.acknowledgeSelfClosingFlag()
+ } else if (name.equals("svg")) {
+ try tb.reconstructFormattingElements()
+ // todo: handle A start tag whose tag name is "svg" (xlink, svg)
+ try tb.insert(startTag)
+ tb.tokeniser.acknowledgeSelfClosingFlag()
+ } else if Constants.InBodyStartDrop.contains(name) {
+ tb.error(self)
+ return false
+ } else {
+ try tb.reconstructFormattingElements()
+ try tb.insert(startTag)
+ }
+ } else {
+ try tb.reconstructFormattingElements()
+ try tb.insert(startTag)
+ }
+ break
+
+ case .EndTag:
+ let endTag: Token.EndTag = t.asEndTag()
+ if let name = endTag.normalName() {
+ if Constants.InBodyEndAdoptionFormatters.contains(name) {
+ // Adoption Agency Algorithm.
+ for _ in 0..<8 {
+ let formatEl: Element? = tb.getActiveFormattingElement(name)
+ if (formatEl == nil) {
+ return anyOtherEndTag(t, tb)
+ } else if (!tb.onStack(formatEl!)) {
+ tb.error(self)
+ tb.removeFromActiveFormattingElements(formatEl!)
+ return true
+ } else if (try !tb.inScope(formatEl!.nodeName())) {
+ tb.error(self)
+ return false
+ } else if (tb.currentElement() != formatEl!) {
+ tb.error(self)
+ }
+
+ var furthestBlock: Element? = nil
+ var commonAncestor: Element? = nil
+ var seenFormattingElement: Bool = false
+ let stack: Array
and processing fake end tag
+ return false
+ }
+ tb.generateImpliedEndTags()
+ if (!name.equals(tb.currentElement()?.nodeName())) {
+ tb.error(self)
+ }
+ tb.popStackToClose(name)
+ tb.clearFormattingElementsToLastMarker()
+ tb.transition(.InRow)
+ } else if let name = name, TagSets.tableMix7.contains(name) {
+ tb.error(self)
+ return false
+ } else if let name = name, TagSets.table.contains(name) {
+ if (try !tb.inTableScope(name)) {
+ tb.error(self)
+ return false
+ }
+ try closeCell(tb)
+ return try tb.process(t)
+ } else {
+ return try anythingElse(t, tb)
+ }
+ } else if let nName = t.startTagNormalName(), TagSets.tableRowsAndCols.contains(nName) {
+ if (try !(tb.inTableScope("td") || tb.inTableScope("th"))) {
+ tb.error(self)
+ return false
+ }
+ try closeCell(tb)
+ return try tb.process(t)
+ } else {
+ return try anythingElse(t, tb)
+ }
+ return true
+ case .InSelect:
+
+ func anythingElse(_ t: Token, _ tb: HtmlTreeBuilder) -> Bool {
+ tb.error(self)
+ return false
+ }
+
+ switch (t.type) {
+ case .Char:
+ let c: Token.Char = t.asCharacter()
+ if (HtmlTreeBuilderState.nullString.equals(c.getData())) {
+ tb.error(self)
+ return false
+ } else {
+ try tb.insert(c)
+ }
+ break
+ case .Comment:
+ try tb.insert(t.asComment())
+ break
+ case .Doctype:
+ tb.error(self)
+ return false
+ case .StartTag:
+ let start: Token.StartTag = t.asStartTag()
+ let name: String? = start.normalName()
+ if ("html".equals(name)) {
+ return try tb.process(start, .InBody)
+ } else if ("option".equals(name)) {
+ try tb.processEndTag("option")
+ try tb.insert(start)
+ } else if ("optgroup".equals(name)) {
+ if ("option".equals(tb.currentElement()?.nodeName())) {
+ try tb.processEndTag("option")
+ } else if ("optgroup".equals(tb.currentElement()?.nodeName())) {
+ try tb.processEndTag("optgroup")
+ }
+ try tb.insert(start)
+ } else if ("select".equals(name)) {
+ tb.error(self)
+ return try tb.processEndTag("select")
+ } else if let name = name, TagSets.inputKeygenTextarea.contains(name) {
+ tb.error(self)
+ if (try !tb.inSelectScope("select")) {
+ return false // frag
+ }
+ try tb.processEndTag("select")
+ return try tb.process(start)
+ } else if ("script".equals(name)) {
+ return try tb.process(t, .InHead)
+ } else {
+ return anythingElse(t, tb)
+ }
+ break
+ case .EndTag:
+ let end: Token.EndTag = t.asEndTag()
+ let name = end.normalName()
+ if ("optgroup".equals(name)) {
+ if ("option".equals(tb.currentElement()?.nodeName()) && tb.currentElement() != nil && tb.aboveOnStack(tb.currentElement()!) != nil && "optgroup".equals(tb.aboveOnStack(tb.currentElement()!)?.nodeName())) {
+ try tb.processEndTag("option")
+ }
+ if ("optgroup".equals(tb.currentElement()?.nodeName())) {
+ tb.pop()
+ } else {
+ tb.error(self)
+ }
+ } else if ("option".equals(name)) {
+ if ("option".equals(tb.currentElement()?.nodeName())) {
+ tb.pop()
+ } else {
+ tb.error(self)
+ }
+ } else if ("select".equals(name)) {
+ if (try !tb.inSelectScope(name!)) {
+ tb.error(self)
+ return false
+ } else {
+ tb.popStackToClose(name!)
+ tb.resetInsertionMode()
+ }
+ } else {
+ return anythingElse(t, tb)
+ }
+ break
+ case .EOF:
+ if (!"html".equals(tb.currentElement()?.nodeName())) {
+ tb.error(self)
+ }
+ break
+// default:
+// return anythingElse(t, tb)
+ }
+ return true
+ case .InSelectInTable:
+ if let nName = t.startTagNormalName(), TagSets.tableMix8.contains(nName) {
+ tb.error(self)
+ try tb.processEndTag("select")
+ return try tb.process(t)
+ } else if let nName = t.endTagNormalName(), TagSets.tableMix8.contains(nName) {
+ tb.error(self)
+ if try tb.inTableScope(nName) {
+ try tb.processEndTag("select")
+ return try (tb.process(t))
+ } else {
+ return false
+ }
+ } else {
+ return try tb.process(t, .InSelect)
+ }
+ case .AfterBody:
+ if (HtmlTreeBuilderState.isWhitespace(t)) {
+ return try tb.process(t, .InBody)
+ } else if (t.isComment()) {
+ try tb.insert(t.asComment()) // into html node
+ } else if (t.isDoctype()) {
+ tb.error(self)
+ return false
+ } else if t.startTagNormalName() == "html" {
+ return try tb.process(t, .InBody)
+ } else if t.endTagNormalName() == "html" {
+ if (tb.isFragmentParsing()) {
+ tb.error(self)
+ return false
+ } else {
+ tb.transition(.AfterAfterBody)
+ }
+ } else if (t.isEOF()) {
+ // chillax! we're done
+ } else {
+ tb.error(self)
+ tb.transition(.InBody)
+ return try tb.process(t)
+ }
+ return true
+ case .InFrameset:
+
+ if (HtmlTreeBuilderState.isWhitespace(t)) {
+ try tb.insert(t.asCharacter())
+ } else if (t.isComment()) {
+ try tb.insert(t.asComment())
+ } else if (t.isDoctype()) {
+ tb.error(self)
+ return false
+ } else if (t.isStartTag()) {
+ let start: Token.StartTag = t.asStartTag()
+ let name: String? = start.normalName()
+ if ("html".equals(name)) {
+ return try tb.process(start, .InBody)
+ } else if ("frameset".equals(name)) {
+ try tb.insert(start)
+ } else if ("frame".equals(name)) {
+ try tb.insertEmpty(start)
+ } else if ("noframes".equals(name)) {
+ return try tb.process(start, .InHead)
+ } else {
+ tb.error(self)
+ return false
+ }
+ } else if t.endTagNormalName() == "frameset" {
+ if ("html".equals(tb.currentElement()?.nodeName())) { // frag
+ tb.error(self)
+ return false
+ } else {
+ tb.pop()
+ if (!tb.isFragmentParsing() && !"frameset".equals(tb.currentElement()?.nodeName())) {
+ tb.transition(.AfterFrameset)
+ }
+ }
+ } else if (t.isEOF()) {
+ if (!"html".equals(tb.currentElement()?.nodeName())) {
+ tb.error(self)
+ return true
+ }
+ } else {
+ tb.error(self)
+ return false
+ }
+ return true
+ case .AfterFrameset:
+
+ if (HtmlTreeBuilderState.isWhitespace(t)) {
+ try tb.insert(t.asCharacter())
+ } else if (t.isComment()) {
+ try tb.insert(t.asComment())
+ } else if (t.isDoctype()) {
+ tb.error(self)
+ return false
+ } else if t.startTagNormalName() == "html" {
+ return try tb.process(t, .InBody)
+ } else if t.endTagNormalName() == "html" {
+ tb.transition(.AfterAfterFrameset)
+ } else if t.startTagNormalName() == "noframes" {
+ return try tb.process(t, .InHead)
+ } else if (t.isEOF()) {
+ // cool your heels, we're complete
+ } else {
+ tb.error(self)
+ return false
+ }
+ return true
+ case .AfterAfterBody:
+
+ if (t.isComment()) {
+ try tb.insert(t.asComment())
+ } else if (t.isDoctype() || HtmlTreeBuilderState.isWhitespace(t) || (t.isStartTag() && "html".equals(t.asStartTag().normalName()))) {
+ return try tb.process(t, .InBody)
+ } else if (t.isEOF()) {
+ // nice work chuck
+ } else {
+ tb.error(self)
+ tb.transition(.InBody)
+ return try tb.process(t)
+ }
+ return true
+ case .AfterAfterFrameset:
+
+ if (t.isComment()) {
+ try tb.insert(t.asComment())
+ } else if (t.isDoctype() || HtmlTreeBuilderState.isWhitespace(t) || (t.startTagNormalName() == "html")) {
+ return try tb.process(t, .InBody)
+ } else if (t.isEOF()) {
+ // nice work chuck
+ } else if t.startTagNormalName() == "noframes" {
+ return try tb.process(t, .InHead)
+ } else {
+ tb.error(self)
+ return false
+ }
+ return true
+ case .ForeignContent:
+ return true
+ // todo: implement. Also how do we get here?
+ }
+
+ }
+
+ private static func isWhitespace(_ t: Token) -> Bool {
+ if (t.isCharacter()) {
+ let data: String? = t.asCharacter().getData()
+ return isWhitespace(data)
+ }
+ return false
+ }
+
+ private static func isWhitespace(_ data: String?) -> Bool {
+ // todo: self checks more than spec - UnicodeScalar.BackslashT, "\n", "\f", "\r", " "
+ if let data = data {
+ for c in data {
+ if (!StringUtil.isWhitespace(c)) {
+ return false}
+ }
+ }
+ return true
+ }
+
+ private static func handleRcData(_ startTag: Token.StartTag, _ tb: HtmlTreeBuilder)throws {
+ try tb.insert(startTag)
+ tb.tokeniser.transition(TokeniserState.Rcdata)
+ tb.markInsertionMode()
+ tb.transition(.Text)
+ }
+
+ private static func handleRawtext(_ startTag: Token.StartTag, _ tb: HtmlTreeBuilder)throws {
+ try tb.insert(startTag)
+ tb.tokeniser.transition(TokeniserState.Rawtext)
+ tb.markInsertionMode()
+ tb.transition(.Text)
+ }
+
+ // lists of tags to search through. A little harder to read here, but causes less GC than dynamic varargs.
+ // was contributing around 10% of parse GC load.
+ fileprivate final class Constants {
+ fileprivate static let InBodyStartToHead: [String] = ["base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title"]
+ fileprivate static let InBodyStartPClosers: [String] = ["address", "article", "aside", "blockquote", "center", "details", "dir", "div", "dl",
+ "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu", "nav", "ol",
+ "p", "section", "summary", "ul"]
+ fileprivate static let Headings: [String] = ["h1", "h2", "h3", "h4", "h5", "h6"]
+ fileprivate static let InBodyStartPreListing: [String] = ["pre", "listing"]
+ fileprivate static let InBodyStartLiBreakers: [String] = ["address", "div", "p"]
+ fileprivate static let DdDt: [String] = ["dd", "dt"]
+ fileprivate static let Formatters: [String] = ["b", "big", "code", "em", "font", "i", "s", "small", "strike", "strong", "tt", "u"]
+ fileprivate static let InBodyStartApplets: [String] = ["applet", "marquee", "object"]
+ fileprivate static let InBodyStartEmptyFormatters: [String] = ["area", "br", "embed", "img", "keygen", "wbr"]
+ fileprivate static let InBodyStartMedia: [String] = ["param", "source", "track"]
+ fileprivate static let InBodyStartInputAttribs: [String] = ["name", "action", "prompt"]
+ fileprivate static let InBodyStartOptions: [String] = ["optgroup", "option"]
+ fileprivate static let InBodyStartRuby: [String] = ["rp", "rt"]
+ fileprivate static let InBodyStartDrop: [String] = ["caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr"]
+ fileprivate static let InBodyEndClosers: [String] = ["address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div",
+ "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu",
+ "nav", "ol", "pre", "section", "summary", "ul"]
+ fileprivate static let InBodyEndAdoptionFormatters: [String] = ["a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u"]
+ fileprivate static let InBodyEndTableFosters: [String] = ["table", "tbody", "tfoot", "thead", "tr"]
+ }
+}
+
+fileprivate extension Token {
+
+ func endTagNormalName() -> String? {
+ guard isEndTag() else { return nil }
+ return asEndTag().normalName()
+ }
+
+ func startTagNormalName() -> String? {
+ guard isStartTag() else { return nil }
+ return asStartTag().normalName()
+ }
+
+}
diff --git a/Swiftgram/SwiftSoup/Sources/HttpStatusException.swift b/Swiftgram/SwiftSoup/Sources/HttpStatusException.swift
new file mode 100644
index 0000000000..7d52dcac24
--- /dev/null
+++ b/Swiftgram/SwiftSoup/Sources/HttpStatusException.swift
@@ -0,0 +1,10 @@
+//
+// HttpStatusException.swift
+// SwifSoup
+//
+// Created by Nabil Chatbi on 29/09/16.
+// Copyright © 2016 Nabil Chatbi.. All rights reserved.
+//
+
+import Foundation
+//TODO:
diff --git a/Swiftgram/SwiftSoup/Sources/Info.plist b/Swiftgram/SwiftSoup/Sources/Info.plist
new file mode 100644
index 0000000000..bfe6ad8b1d
--- /dev/null
+++ b/Swiftgram/SwiftSoup/Sources/Info.plist
@@ -0,0 +1,26 @@
+
+
+ abs
,
+ * which is a shortcut to the {@link #absUrl} method.
+ *
+ *
+ * @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 String url = a.attr("abs:href");
<a href>
or
+ * <img src>
).
+ * String absUrl = linkEl.absUrl("href");
+ * 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.
+ * abs:
prefix, e.g.:
+ * String absUrl = linkEl.attr("abs:href");
+ *