Archived
Initial commit; substitutions can be reviewed, no further functionality implemented yet
This commit is contained in:
Generated
+21
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// ArrayExt.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 05/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Array where Element : Equatable {
|
||||
func lastIndexOf(_ e: Element) -> Int {
|
||||
for pos in (0..<self.count).reversed() {
|
||||
let next = self[pos]
|
||||
if (next == e) {
|
||||
return pos
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
Generated
+148
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// Attribute.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class Attribute {
|
||||
/// The element type of a dictionary: a tuple containing an individual
|
||||
/// key-value pair.
|
||||
|
||||
static let booleanAttributes: [String] = [
|
||||
"allowfullscreen", "async", "autofocus", "checked", "compact", "declare", "default", "defer", "disabled",
|
||||
"formnovalidate", "hidden", "inert", "ismap", "itemscope", "multiple", "muted", "nohref", "noresize",
|
||||
"noshade", "novalidate", "nowrap", "open", "readonly", "required", "reversed", "seamless", "selected",
|
||||
"sortable", "truespeed", "typemustmatch"
|
||||
]
|
||||
|
||||
var key: String
|
||||
var value: String
|
||||
|
||||
public init(key: String, value: String) throws {
|
||||
try Validate.notEmpty(string: key)
|
||||
self.key = key.trim()
|
||||
self.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
Get the attribute key.
|
||||
@return the attribute key
|
||||
*/
|
||||
open func getKey() -> String {
|
||||
return key
|
||||
}
|
||||
|
||||
/**
|
||||
Set the attribute key; case is preserved.
|
||||
@param key the new key; must not be null
|
||||
*/
|
||||
open func setKey(key: String) throws {
|
||||
try Validate.notEmpty(string: key)
|
||||
self.key = key.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
Get the attribute value.
|
||||
@return the attribute value
|
||||
*/
|
||||
open func getValue() -> String {
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
Set the attribute value.
|
||||
@param value the new attribute value; must not be null
|
||||
*/
|
||||
@discardableResult
|
||||
open func setValue(value: String) -> String {
|
||||
let old = self.value
|
||||
self.value = value
|
||||
return old
|
||||
}
|
||||
|
||||
/**
|
||||
Get the HTML representation of this attribute; e.g. {@code href="index.html"}.
|
||||
@return HTML
|
||||
*/
|
||||
public func html() -> String {
|
||||
let accum = StringBuilder()
|
||||
html(accum: accum, out: (Document("")).outputSettings())
|
||||
return accum.toString()
|
||||
}
|
||||
|
||||
public func html(accum: StringBuilder, out: OutputSettings ) {
|
||||
accum.append(key)
|
||||
if (!shouldCollapseAttribute(out: out)) {
|
||||
accum.append("=\"")
|
||||
Entities.escape(accum, value, out, true, false, false)
|
||||
accum.append("\"")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Get the string representation of this attribute, implemented as {@link #html()}.
|
||||
@return string
|
||||
*/
|
||||
open func toString() -> String {
|
||||
return html()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Attribute from an unencoded key and a HTML attribute encoded value.
|
||||
* @param unencodedKey assumes the key is not encoded, as can be only run of simple \w chars.
|
||||
* @param encodedValue HTML attribute encoded value
|
||||
* @return attribute
|
||||
*/
|
||||
public static func createFromEncoded(unencodedKey: String, encodedValue: String) throws ->Attribute {
|
||||
let value = try Entities.unescape(string: encodedValue, strict: true)
|
||||
return try Attribute(key: unencodedKey, value: value)
|
||||
}
|
||||
|
||||
public func isDataAttribute() -> Bool {
|
||||
return key.startsWith(Attributes.dataPrefix) && key.count > Attributes.dataPrefix.count
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsible if it's a boolean attribute and value is empty or same as name
|
||||
*
|
||||
* @param out Outputsettings
|
||||
* @return Returns whether collapsible or not
|
||||
*/
|
||||
public final func shouldCollapseAttribute(out: OutputSettings) -> Bool {
|
||||
return ("" == value || value.equalsIgnoreCase(string: key))
|
||||
&& out.syntax() == OutputSettings.Syntax.html
|
||||
&& isBooleanAttribute()
|
||||
}
|
||||
|
||||
public func isBooleanAttribute() -> Bool {
|
||||
return Attribute.booleanAttributes.contains(key)
|
||||
}
|
||||
|
||||
public func hashCode() -> Int {
|
||||
var result = key.hashValue
|
||||
result = 31 * result + value.hashValue
|
||||
return result
|
||||
}
|
||||
|
||||
public func clone() -> Attribute {
|
||||
do {
|
||||
return try Attribute(key: key, value: value)
|
||||
} catch Exception.Error( _, let msg) {
|
||||
print(msg)
|
||||
} catch {
|
||||
|
||||
}
|
||||
return try! Attribute(key: "", value: "")
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: Equatable {
|
||||
static public func == (lhs: Attribute, rhs: Attribute) -> Bool {
|
||||
return lhs.value == rhs.value && lhs.key == rhs.key
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+264
@@ -0,0 +1,264 @@
|
||||
//
|
||||
// Attributes.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* The attributes of an Element.
|
||||
* <p>
|
||||
* Attributes are treated as a map: there can be only one value associated with an attribute key/name.
|
||||
* </p>
|
||||
* <p>
|
||||
* Attribute name and value comparisons are <b>case sensitive</b>. 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.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
*/
|
||||
open class Attributes: NSCopying {
|
||||
|
||||
public static var dataPrefix: String = "data-"
|
||||
|
||||
var attributes: OrderedDictionary<String, Attribute> = OrderedDictionary<String, Attribute>()
|
||||
// linked hash map to preserve insertion order.
|
||||
// null be default as so many elements have no attributes -- saves a good chunk of memory
|
||||
|
||||
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 {
|
||||
let attr: Attribute? = attributes.get(key: key)
|
||||
return attr != nil ? attr!.getValue() : ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
for attrKey in (attributes.keySet()) {
|
||||
if attrKey.equalsIgnoreCase(string: key) {
|
||||
return attributes.get(key: attrKey)!.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 key.
|
||||
@param attribute attribute
|
||||
*/
|
||||
open func put(attribute: Attribute) {
|
||||
attributes.put(value: attribute, forKey: attribute.getKey())
|
||||
}
|
||||
|
||||
/**
|
||||
Remove an attribute by key. <b>Case sensitive.</b>
|
||||
@param key attribute key to remove
|
||||
*/
|
||||
open func remove(key: String)throws {
|
||||
try Validate.notEmpty(string: key)
|
||||
attributes.remove(key: key)
|
||||
}
|
||||
|
||||
/**
|
||||
Remove an attribute by key. <b>Case insensitive.</b>
|
||||
@param key attribute key to remove
|
||||
*/
|
||||
open func removeIgnoreCase(key: String ) throws {
|
||||
try Validate.notEmpty(string: key)
|
||||
for attrKey in attributes.keySet() {
|
||||
if (attrKey.equalsIgnoreCase(string: key)) {
|
||||
attributes.remove(key: attrKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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.containsKey(key: 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 {
|
||||
for attrKey in attributes.keySet() {
|
||||
if (attrKey.equalsIgnoreCase(string: key)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
Get the number of attributes in this set.
|
||||
@return size
|
||||
*/
|
||||
open func size() -> Int {
|
||||
return attributes.count//TODO: check retyrn right size
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
}
|
||||
|
||||
if (incoming.size() == 0) {
|
||||
return
|
||||
}
|
||||
attributes.putAll(all: incoming.attributes)
|
||||
}
|
||||
|
||||
// open func iterator() -> IndexingIterator<Array<Attribute>> {
|
||||
// if (attributes.isEmpty) {
|
||||
// let args: [Attribute] = []
|
||||
// return args.makeIterator()
|
||||
// }
|
||||
// return attributes.orderedValues.makeIterator()
|
||||
// }
|
||||
|
||||
/**
|
||||
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() -> Array<Attribute> {
|
||||
var list: Array<Attribute> = Array(/*attributes.size()*/)
|
||||
for entry in attributes.orderedValues {
|
||||
list.append(entry)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
//Map<String, String>
|
||||
open func dataset() -> Dictionary<String, String> {
|
||||
var dataset = Dictionary<String, String>()
|
||||
for attribute in attributes {
|
||||
let attr = attribute.1
|
||||
if(attr.isDataAttribute()) {
|
||||
let key = attr.getKey().substring(Attributes.dataPrefix.count)
|
||||
dataset[key] = attribute.1.getValue()
|
||||
}
|
||||
}
|
||||
return dataset
|
||||
}
|
||||
|
||||
/**
|
||||
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 attribute in attributes.orderedValues {
|
||||
accum.append(" ")
|
||||
attribute.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: Attributes = o as? Attributes else {return false}
|
||||
return (attributes == that.attributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the hashcode of these attributes, by iterating all attributes and summing their hashcodes.
|
||||
* @return calculated hashcode
|
||||
*/
|
||||
open func hashCode() -> Int {
|
||||
return attributes.hashCode()
|
||||
}
|
||||
|
||||
public func copy(with zone: NSZone? = nil) -> Any {
|
||||
let clone = Attributes()
|
||||
clone.attributes = attributes.clone()
|
||||
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<Attribute> {
|
||||
var list = attributes.orderedValues
|
||||
return AnyIterator {
|
||||
return list.count > 0 ? list.removeFirst() : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// BooleanAttribute.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* A boolean attribute that is written out without any value.
|
||||
*/
|
||||
open class BooleanAttribute: Attribute {
|
||||
/**
|
||||
* Create a new boolean attribute from unencoded (raw) key.
|
||||
* @param key attribute key
|
||||
*/
|
||||
init(key: String) throws {
|
||||
try super.init(key: key, value: "")
|
||||
}
|
||||
|
||||
override public func isBooleanAttribute() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// CharacterExt.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 08/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Character {
|
||||
|
||||
public static let space: Character = " "
|
||||
public static let BackslashT: Character = "\t"
|
||||
public static let BackslashN: Character = "\n"
|
||||
public static let BackslashF: Character = Character(UnicodeScalar(12))
|
||||
public static let BackslashR: Character = "\r"
|
||||
public static let BackshashRBackslashN: Character = "\r\n"
|
||||
|
||||
//http://www.unicode.org/glossary/#supplementary_code_point
|
||||
public static let MIN_SUPPLEMENTARY_CODE_POINT: UInt32 = 0x010000
|
||||
|
||||
/// True for any space character, and the control characters \t, \n, \r, \f, \v.
|
||||
|
||||
var isWhitespace: Bool {
|
||||
switch self {
|
||||
case Character.space, Character.BackslashT, Character.BackslashN, Character.BackslashF, Character.BackslashR: return true
|
||||
case Character.BackshashRBackslashN: return true
|
||||
default: return false
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` if `self` normalized contains a single code unit that is in the category of Decimal Numbers.
|
||||
var isDigit: Bool {
|
||||
|
||||
return isMemberOfCharacterSet(CharacterSet.decimalDigits)
|
||||
|
||||
}
|
||||
|
||||
/// Lowercase `self`.
|
||||
var lowercase: Character {
|
||||
|
||||
let str = String(self).lowercased()
|
||||
return str[str.startIndex]
|
||||
|
||||
}
|
||||
|
||||
/// Return `true` if `self` normalized contains a single code unit that is a member of the supplied character set.
|
||||
///
|
||||
/// - parameter set: The `NSCharacterSet` used to test for membership.
|
||||
/// - returns: `true` if `self` normalized contains a single code unit that is a member of the supplied character set.
|
||||
func isMemberOfCharacterSet(_ set: CharacterSet) -> Bool {
|
||||
|
||||
let normalized = String(self).precomposedStringWithCanonicalMapping
|
||||
let unicodes = normalized.unicodeScalars
|
||||
|
||||
guard unicodes.count == 1 else { return false }
|
||||
return set.contains(UnicodeScalar(unicodes.first!.value)!)
|
||||
|
||||
}
|
||||
|
||||
static func convertFromIntegerLiteral(value: IntegerLiteralType) -> Character {
|
||||
return Character(UnicodeScalar(value)!)
|
||||
}
|
||||
|
||||
static func isLetter(_ char: Character) -> Bool {
|
||||
return char.isLetter()
|
||||
}
|
||||
func isLetter() -> Bool {
|
||||
return self.isMemberOfCharacterSet(CharacterSet.letters)
|
||||
}
|
||||
|
||||
static func isLetterOrDigit(_ char: Character) -> Bool {
|
||||
return char.isLetterOrDigit()
|
||||
}
|
||||
func isLetterOrDigit() -> Bool {
|
||||
if(self.isLetter()) {return true}
|
||||
return self.isDigit
|
||||
}
|
||||
}
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
//
|
||||
// CharacterReader.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 10/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
CharacterReader consumes tokens off a string. To replace the old TokenQueue.
|
||||
*/
|
||||
public final class CharacterReader {
|
||||
private static let empty = ""
|
||||
public static let EOF: UnicodeScalar = "\u{FFFF}"//65535
|
||||
private let input: [UnicodeScalar]
|
||||
private let length: Int
|
||||
private var pos: Int = 0
|
||||
private var mark: Int = 0
|
||||
//private let stringCache: Array<String?> // holds reused strings in this doc, to lessen garbage
|
||||
|
||||
public init(_ input: String) {
|
||||
self.input = Array(input.unicodeScalars)
|
||||
self.length = self.input.count
|
||||
//stringCache = Array(repeating:nil, count:512)
|
||||
}
|
||||
|
||||
public func getPos() -> Int {
|
||||
return self.pos
|
||||
}
|
||||
|
||||
public func isEmpty() -> Bool {
|
||||
return pos >= length
|
||||
}
|
||||
|
||||
public func current() -> UnicodeScalar {
|
||||
return (pos >= length) ? CharacterReader.EOF : input[pos]
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func consume() -> UnicodeScalar {
|
||||
let val = (pos >= length) ? CharacterReader.EOF : input[pos]
|
||||
pos += 1
|
||||
return val
|
||||
}
|
||||
|
||||
public func unconsume() {
|
||||
pos -= 1
|
||||
}
|
||||
|
||||
public func advance() {
|
||||
pos += 1
|
||||
}
|
||||
|
||||
public func markPos() {
|
||||
mark = pos
|
||||
}
|
||||
|
||||
public func rewindToMark() {
|
||||
pos = mark
|
||||
}
|
||||
|
||||
public func consumeAsString() -> String {
|
||||
let p = pos
|
||||
pos+=1
|
||||
return String(input[p])
|
||||
//return String(input, pos+=1, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of characters between the current position and the next instance of the input char
|
||||
* @param c scan target
|
||||
* @return offset between current position and next instance of target. -1 if not found.
|
||||
*/
|
||||
public func nextIndexOf(_ c: UnicodeScalar) -> Int {
|
||||
// doesn't handle scanning for surrogates
|
||||
for i in pos..<length {
|
||||
if (c == input[i]) {
|
||||
return i - pos
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of characters between the current position and the next instance of the input sequence
|
||||
*
|
||||
* @param seq scan target
|
||||
* @return offset between current position and next instance of target. -1 if not found.
|
||||
*/
|
||||
public func nextIndexOf(_ seq: String) -> Int {
|
||||
// doesn't handle scanning for surrogates
|
||||
if(seq.isEmpty) {return -1}
|
||||
let startChar: UnicodeScalar = seq.unicodeScalar(0)
|
||||
for var offset in pos..<length {
|
||||
// scan to first instance of startchar:
|
||||
if (startChar != input[offset]) {
|
||||
offset+=1
|
||||
while(offset < length && startChar != input[offset]) { offset+=1 }
|
||||
}
|
||||
var i = offset + 1
|
||||
let last = i + seq.unicodeScalars.count-1
|
||||
if (offset < length && last <= length) {
|
||||
var j = 1
|
||||
while i < last && seq.unicodeScalar(j) == input[i] {
|
||||
j+=1
|
||||
i+=1
|
||||
}
|
||||
// found full sequence
|
||||
if (i == last) {
|
||||
return offset - pos
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
public func consumeTo(_ c: UnicodeScalar) -> String {
|
||||
let offset = nextIndexOf(c)
|
||||
if (offset != -1) {
|
||||
let consumed = cacheString(pos, offset)
|
||||
pos += offset
|
||||
return consumed
|
||||
} else {
|
||||
return consumeToEnd()
|
||||
}
|
||||
}
|
||||
|
||||
public func consumeTo(_ seq: String) -> String {
|
||||
let offset = nextIndexOf(seq)
|
||||
if (offset != -1) {
|
||||
let consumed = cacheString(pos, offset)
|
||||
pos += offset
|
||||
return consumed
|
||||
} else {
|
||||
return consumeToEnd()
|
||||
}
|
||||
}
|
||||
|
||||
public func consumeToAny(_ chars: UnicodeScalar...) -> String {
|
||||
return consumeToAny(chars)
|
||||
}
|
||||
public func consumeToAny(_ chars: [UnicodeScalar]) -> String {
|
||||
let start: Int = pos
|
||||
let remaining: Int = length
|
||||
let val = input
|
||||
OUTER: while (pos < remaining) {
|
||||
if chars.contains(val[pos]) {
|
||||
break OUTER
|
||||
}
|
||||
// for c in chars {
|
||||
// if (val[pos] == c){
|
||||
// break OUTER
|
||||
// }
|
||||
// }
|
||||
pos += 1
|
||||
}
|
||||
|
||||
return pos > start ? cacheString(start, pos-start) : CharacterReader.empty
|
||||
}
|
||||
|
||||
public func consumeToAnySorted(_ chars: UnicodeScalar...) -> String {
|
||||
return consumeToAnySorted(chars)
|
||||
}
|
||||
public func consumeToAnySorted(_ chars: [UnicodeScalar]) -> String {
|
||||
let start = pos
|
||||
let remaining = length
|
||||
let val = input
|
||||
|
||||
while (pos < remaining) {
|
||||
|
||||
if chars.contains(val[pos]) {
|
||||
break
|
||||
}
|
||||
pos += 1
|
||||
}
|
||||
|
||||
return pos > start ? cacheString(start, pos-start) : CharacterReader.empty
|
||||
}
|
||||
|
||||
public func consumeData() -> String {
|
||||
// &, <, null
|
||||
let start = pos
|
||||
let remaining = length
|
||||
let val = input
|
||||
|
||||
while (pos < remaining) {
|
||||
let c: UnicodeScalar = val[pos]
|
||||
if (c == UnicodeScalar.Ampersand || c == UnicodeScalar.LessThan || c == TokeniserStateVars.nullScalr) {
|
||||
break
|
||||
}
|
||||
pos += 1
|
||||
}
|
||||
|
||||
return pos > start ? cacheString(start, pos-start) : CharacterReader.empty
|
||||
}
|
||||
|
||||
public func consumeTagName() -> String {
|
||||
// '\t', '\n', '\r', '\f', ' ', '/', '>', nullChar
|
||||
let start = pos
|
||||
let remaining = length
|
||||
let val = input
|
||||
|
||||
while (pos < remaining) {
|
||||
let c: UnicodeScalar = val[pos]
|
||||
if (c == UnicodeScalar.BackslashT || c == UnicodeScalar.BackslashN || c == UnicodeScalar.BackslashR || c == UnicodeScalar.BackslashF || c == UnicodeScalar.Space || c == UnicodeScalar.Slash || c == UnicodeScalar.GreaterThan || c == TokeniserStateVars.nullScalr) {
|
||||
break
|
||||
}
|
||||
pos += 1
|
||||
}
|
||||
return pos > start ? cacheString(start, pos-start) : CharacterReader.empty
|
||||
}
|
||||
|
||||
public func consumeToEnd() -> String {
|
||||
let data = cacheString(pos, length-pos)
|
||||
pos = length
|
||||
return data
|
||||
}
|
||||
|
||||
public func consumeLetterSequence() -> String {
|
||||
let start = pos
|
||||
while (pos < length) {
|
||||
let c: UnicodeScalar = input[pos]
|
||||
if ((c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c.isMemberOfCharacterSet(CharacterSet.letters)) {
|
||||
pos += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return cacheString(start, pos - start)
|
||||
}
|
||||
|
||||
public func consumeLetterThenDigitSequence() -> String {
|
||||
let start = pos
|
||||
while (pos < length) {
|
||||
let c = input[pos]
|
||||
if ((c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c.isMemberOfCharacterSet(CharacterSet.letters)) {
|
||||
pos += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
while (!isEmpty()) {
|
||||
let c = input[pos]
|
||||
if (c >= "0" && c <= "9") {
|
||||
pos += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return cacheString(start, pos - start)
|
||||
}
|
||||
|
||||
public func consumeHexSequence() -> String {
|
||||
let start = pos
|
||||
while (pos < length) {
|
||||
let c = input[pos]
|
||||
if ((c >= "0" && c <= "9") || (c >= "A" && c <= "F") || (c >= "a" && c <= "f")) {
|
||||
pos+=1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return cacheString(start, pos - start)
|
||||
}
|
||||
|
||||
public func consumeDigitSequence() -> String {
|
||||
let start = pos
|
||||
while (pos < length) {
|
||||
let c = input[pos]
|
||||
if (c >= "0" && c <= "9") {
|
||||
pos+=1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return cacheString(start, pos - start)
|
||||
}
|
||||
|
||||
public func matches(_ c: UnicodeScalar) -> Bool {
|
||||
return !isEmpty() && input[pos] == c
|
||||
|
||||
}
|
||||
|
||||
public func matches(_ seq: String) -> Bool {
|
||||
let scanLength = seq.unicodeScalars.count
|
||||
if (scanLength > length - pos) {
|
||||
return false
|
||||
}
|
||||
|
||||
for offset in 0..<scanLength {
|
||||
if (seq.unicodeScalar(offset) != input[pos+offset]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func matchesIgnoreCase(_ seq: String ) -> Bool {
|
||||
|
||||
let scanLength = seq.unicodeScalars.count
|
||||
if(scanLength == 0) {
|
||||
return false
|
||||
}
|
||||
if (scanLength > length - pos) {
|
||||
return false
|
||||
}
|
||||
|
||||
for offset in 0..<scanLength {
|
||||
let upScan: UnicodeScalar = seq.unicodeScalar(offset).uppercase
|
||||
let upTarget: UnicodeScalar = input[pos+offset].uppercase
|
||||
if (upScan != upTarget) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func matchesAny(_ seq: UnicodeScalar...) -> Bool {
|
||||
if (isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
let c: UnicodeScalar = input[pos]
|
||||
for seek in seq {
|
||||
if (seek == c) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func matchesAnySorted(_ seq: [UnicodeScalar]) -> Bool {
|
||||
return !isEmpty() && seq.contains(input[pos])
|
||||
}
|
||||
|
||||
public func matchesLetter() -> Bool {
|
||||
if (isEmpty()) {
|
||||
return false
|
||||
}
|
||||
let c = input[pos]
|
||||
return (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c.isMemberOfCharacterSet(CharacterSet.letters)
|
||||
}
|
||||
|
||||
public func matchesDigit() -> Bool {
|
||||
if (isEmpty()) {
|
||||
return false
|
||||
}
|
||||
let c = input[pos]
|
||||
return (c >= "0" && c <= "9")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func matchConsume(_ seq: String) -> Bool {
|
||||
if (matches(seq)) {
|
||||
pos += seq.unicodeScalars.count
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func matchConsumeIgnoreCase(_ seq: String) -> Bool {
|
||||
if (matchesIgnoreCase(seq)) {
|
||||
pos += seq.unicodeScalars.count
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func containsIgnoreCase(_ seq: String ) -> Bool {
|
||||
// used to check presence of </title>, </style>. only finds consistent case.
|
||||
let loScan = seq.lowercased(with: Locale(identifier: "en"))
|
||||
let hiScan = seq.uppercased(with: Locale(identifier: "eng"))
|
||||
return (nextIndexOf(loScan) > -1) || (nextIndexOf(hiScan) > -1)
|
||||
}
|
||||
|
||||
public func toString() -> String {
|
||||
return String(input[pos..<length])
|
||||
//return String.unicodescalars(Array(input[pos..<length]))
|
||||
//return input.string(pos, length - pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches short strings, as a flywheel pattern, to reduce GC load. Just for this doc, to prevent leaks.
|
||||
* <p />
|
||||
* Simplistic, and on hash collisions just falls back to creating a new string, vs a full HashMap with Entry list.
|
||||
* That saves both having to create objects as hash keys, and running through the entry list, at the expense of
|
||||
* some more duplicates.
|
||||
*/
|
||||
private func cacheString(_ start: Int, _ count: Int) -> String {
|
||||
return String(input[start..<start+count])
|
||||
// Too Slow
|
||||
// var cache: [String?] = stringCache
|
||||
//
|
||||
// // limit (no cache):
|
||||
// if (count > CharacterReader.maxCacheLen) {
|
||||
// return String(val[start..<start+count].flatMap { Character($0) })
|
||||
// }
|
||||
//
|
||||
// // calculate hash:
|
||||
// var hash: Int = 0
|
||||
// var offset = start
|
||||
// for _ in 0..<count {
|
||||
// let ch = val[offset].value
|
||||
// hash = Int.addWithOverflow(Int.multiplyWithOverflow(31, hash).0, Int(ch)).0
|
||||
// offset+=1
|
||||
// }
|
||||
//
|
||||
// // get from cache
|
||||
// hash = abs(hash)
|
||||
// let i = hash % cache.count
|
||||
// let index: Int = abs(i) //Int(hash & Int(cache.count) - 1)
|
||||
// var cached = cache[index]
|
||||
//
|
||||
// if (cached == nil) { // miss, add
|
||||
// cached = String(val[start..<start+count].flatMap { Character($0) })
|
||||
// //cached = val.string(start, count)
|
||||
// cache[Int(index)] = cached
|
||||
// } else { // hashcode hit, check equality
|
||||
// if (rangeEquals(start, count, cached!)) { // hit
|
||||
// return cached!
|
||||
// } else { // hashcode conflict
|
||||
// cached = String(val[start..<start+count].flatMap { Character($0) })
|
||||
// //cached = val.string(start, count)
|
||||
// cache[index] = cached // update the cache, as recently used strings are more likely to show up again
|
||||
// }
|
||||
// }
|
||||
// return cached!
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Check if the value of the provided range equals the string.
|
||||
// */
|
||||
// public func rangeEquals(_ start: Int, _ count: Int, _ cached: String) -> Bool {
|
||||
// if (count == cached.unicodeScalars.count) {
|
||||
// var count = count
|
||||
// let one = input
|
||||
// var i = start
|
||||
// var j = 0
|
||||
// while (count != 0) {
|
||||
// count -= 1
|
||||
// if (one[i] != cached.unicodeScalar(j) ) {
|
||||
// return false
|
||||
// }
|
||||
// j += 1
|
||||
// i += 1
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
}
|
||||
|
||||
extension CharacterReader: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
return self.toString()
|
||||
}
|
||||
}
|
||||
Generated
+150
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// Cleaner.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 15/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class Cleaner {
|
||||
fileprivate let whitelist: Whitelist
|
||||
|
||||
/**
|
||||
Create a new cleaner, that sanitizes documents using the supplied whitelist.
|
||||
@param whitelist white-list to clean with
|
||||
*/
|
||||
public init(_ whitelist: Whitelist) {
|
||||
self.whitelist = whitelist
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new, clean document, from the original dirty document, containing only elements allowed by the whitelist.
|
||||
The original document is not modified. Only elements from the dirt document's <code>body</code> are used.
|
||||
@param dirtyDocument Untrusted base document to clean.
|
||||
@return cleaned document.
|
||||
*/
|
||||
public func clean(_ dirtyDocument: Document)throws->Document {
|
||||
//Validate.notNull(dirtyDocument)
|
||||
let clean: Document = Document.createShell(dirtyDocument.getBaseUri())
|
||||
if (dirtyDocument.body() != nil && clean.body() != nil) // frameset documents won't have a body. the clean doc will have empty body.
|
||||
{
|
||||
try copySafeNodes(dirtyDocument.body()!, clean.body()!)
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
/**
|
||||
Determines if the input document is valid, against the whitelist. It is considered valid if all the tags and attributes
|
||||
in the input HTML are allowed by the whitelist.
|
||||
<p>
|
||||
This method can be used as a validator for user input forms. An invalid document will still be cleaned successfully
|
||||
using the {@link #clean(Document)} document. If using as a validator, it is recommended to still clean the document
|
||||
to ensure enforced attributes are set correctly, and that the output is tidied.
|
||||
</p>
|
||||
@param dirtyDocument document to test
|
||||
@return true if no tags or attributes need to be removed; false if they do
|
||||
*/
|
||||
public func isValid(_ dirtyDocument: Document)throws->Bool {
|
||||
//Validate.notNull(dirtyDocument)
|
||||
let clean: Document = Document.createShell(dirtyDocument.getBaseUri())
|
||||
let numDiscarded: Int = try copySafeNodes(dirtyDocument.body()!, clean.body()!)
|
||||
return numDiscarded == 0
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
fileprivate func copySafeNodes(_ source: Element, _ dest: Element)throws->Int {
|
||||
let cleaningVisitor: Cleaner.CleaningVisitor = Cleaner.CleaningVisitor(source, dest, self)
|
||||
let traversor: NodeTraversor = NodeTraversor(cleaningVisitor)
|
||||
try traversor.traverse(source)
|
||||
return cleaningVisitor.numDiscarded
|
||||
}
|
||||
|
||||
fileprivate func createSafeElement(_ sourceEl: Element)throws->ElementMeta {
|
||||
let sourceTag: String = sourceEl.tagName()
|
||||
let destAttrs: Attributes = Attributes()
|
||||
let dest: Element = try Element(Tag.valueOf(sourceTag), sourceEl.getBaseUri(), destAttrs)
|
||||
var numDiscarded: Int = 0
|
||||
|
||||
if let sourceAttrs = sourceEl.getAttributes() {
|
||||
for sourceAttr: Attribute in sourceAttrs {
|
||||
if (try whitelist.isSafeAttribute(sourceTag, sourceEl, sourceAttr)) {
|
||||
destAttrs.put(attribute: sourceAttr)
|
||||
} else {
|
||||
numDiscarded+=1
|
||||
}
|
||||
}
|
||||
}
|
||||
let enforcedAttrs: Attributes = try whitelist.getEnforcedAttributes(sourceTag)
|
||||
destAttrs.addAll(incoming: enforcedAttrs)
|
||||
|
||||
return ElementMeta(dest, numDiscarded)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Cleaner {
|
||||
fileprivate final class CleaningVisitor: NodeVisitor {
|
||||
var numDiscarded: Int = 0
|
||||
let root: Element
|
||||
var destination: Element? // current element to append nodes to
|
||||
|
||||
private var cleaner: Cleaner
|
||||
|
||||
public init(_ root: Element, _ destination: Element, _ cleaner: Cleaner) {
|
||||
self.root = root
|
||||
self.destination = destination
|
||||
self.cleaner = cleaner
|
||||
}
|
||||
|
||||
public func head(_ source: Node, _ depth: Int)throws {
|
||||
if let sourceEl = (source as? Element) {
|
||||
if (cleaner.whitelist.isSafeTag(sourceEl.tagName())) { // safe, clone and copy safe attrs
|
||||
let meta: Cleaner.ElementMeta = try cleaner.createSafeElement(sourceEl)
|
||||
let destChild: Element = meta.el
|
||||
try destination?.appendChild(destChild)
|
||||
|
||||
numDiscarded += meta.numAttribsDiscarded
|
||||
destination = destChild
|
||||
} else if (source != root) { // not a safe tag, so don't add. don't count root against discarded.
|
||||
numDiscarded+=1
|
||||
}
|
||||
} else if let sourceText = (source as? TextNode) {
|
||||
let destText: TextNode = TextNode(sourceText.getWholeText(), source.getBaseUri())
|
||||
try destination?.appendChild(destText)
|
||||
} else if let sourceData = (source as? DataNode) {
|
||||
if sourceData.parent() != nil && cleaner.whitelist.isSafeTag(sourceData.parent()!.nodeName()) {
|
||||
//let sourceData: DataNode = (DataNode) source
|
||||
let destData: DataNode = DataNode(sourceData.getWholeData(), source.getBaseUri())
|
||||
try destination?.appendChild(destData)
|
||||
} else {
|
||||
numDiscarded+=1
|
||||
}
|
||||
} else { // else, we don't care about comments, xml proc instructions, etc
|
||||
numDiscarded+=1
|
||||
}
|
||||
}
|
||||
|
||||
public func tail(_ source: Node, _ depth: Int)throws {
|
||||
if let x = (source as? Element) {
|
||||
if cleaner.whitelist.isSafeTag(x.nodeName()) {
|
||||
// would have descended, so pop destination stack
|
||||
destination = destination?.parent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Cleaner {
|
||||
fileprivate struct ElementMeta {
|
||||
let el: Element
|
||||
let numAttribsDiscarded: Int
|
||||
|
||||
init(_ el: Element, _ numAttribsDiscarded: Int) {
|
||||
self.el = el
|
||||
self.numAttribsDiscarded = numAttribsDiscarded
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+59
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// Collector.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 22/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Collects a list of elements that match the supplied criteria.
|
||||
*
|
||||
*/
|
||||
open class Collector {
|
||||
|
||||
private init() {
|
||||
}
|
||||
|
||||
/**
|
||||
Build a list of elements, by visiting root and every descendant of root, and testing it against the evaluator.
|
||||
@param eval Evaluator to test elements against
|
||||
@param root root of tree to descend
|
||||
@return list of matches; empty if none
|
||||
*/
|
||||
public static func collect (_ eval: Evaluator, _ root: Element)throws->Elements {
|
||||
let elements: Elements = Elements()
|
||||
try NodeTraversor(Accumulator(root, elements, eval)).traverse(root)
|
||||
return elements
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class Accumulator: NodeVisitor {
|
||||
private let root: Element
|
||||
private let elements: Elements
|
||||
private let eval: Evaluator
|
||||
|
||||
init(_ root: Element, _ elements: Elements, _ eval: Evaluator) {
|
||||
self.root = root
|
||||
self.elements = elements
|
||||
self.eval = eval
|
||||
}
|
||||
|
||||
public func head(_ node: Node, _ depth: Int) {
|
||||
guard let el = node as? Element else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
if try eval.matches(root, el) {
|
||||
elements.add(el)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
public func tail(_ node: Node, _ depth: Int) {
|
||||
// void
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// CombiningEvaluator.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 23/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Base combining (and, or) evaluator.
|
||||
*/
|
||||
public class CombiningEvaluator: Evaluator {
|
||||
|
||||
public private(set) var evaluators: Array<Evaluator>
|
||||
var num: Int = 0
|
||||
|
||||
public override init() {
|
||||
evaluators = Array<Evaluator>()
|
||||
super.init()
|
||||
}
|
||||
|
||||
public init(_ evaluators: Array<Evaluator>) {
|
||||
self.evaluators = evaluators
|
||||
super.init()
|
||||
updateNumEvaluators()
|
||||
}
|
||||
|
||||
public init(_ evaluators: Evaluator...) {
|
||||
self.evaluators = evaluators
|
||||
super.init()
|
||||
updateNumEvaluators()
|
||||
}
|
||||
|
||||
func rightMostEvaluator() -> Evaluator? {
|
||||
return num > 0 && evaluators.count > 0 ? evaluators[num - 1] : nil
|
||||
}
|
||||
|
||||
func replaceRightMostEvaluator(_ replacement: Evaluator) {
|
||||
evaluators[num - 1] = replacement
|
||||
}
|
||||
|
||||
func updateNumEvaluators() {
|
||||
// used so we don't need to bash on size() for every match test
|
||||
num = evaluators.count
|
||||
}
|
||||
|
||||
public final class And: CombiningEvaluator {
|
||||
public override init(_ evaluators: [Evaluator]) {
|
||||
super.init(evaluators)
|
||||
}
|
||||
|
||||
public override init(_ evaluators: Evaluator...) {
|
||||
super.init(evaluators)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ node: Element) -> Bool {
|
||||
for index in 0..<num {
|
||||
let evaluator = evaluators[index]
|
||||
do {
|
||||
if (try !evaluator.matches(root, node)) {
|
||||
return false
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
let array: [String] = evaluators.map { String($0.toString()) }
|
||||
return StringUtil.join(array, sep: " ")
|
||||
}
|
||||
}
|
||||
|
||||
public final class Or: CombiningEvaluator {
|
||||
/**
|
||||
* Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the OR.
|
||||
* @param evaluators initial OR clause (these are wrapped into an AND evaluator).
|
||||
*/
|
||||
public override init(_ evaluators: [Evaluator]) {
|
||||
super.init()
|
||||
if num > 1 {
|
||||
self.evaluators.append(And(evaluators))
|
||||
} else { // 0 or 1
|
||||
self.evaluators.append(contentsOf: evaluators)
|
||||
}
|
||||
updateNumEvaluators()
|
||||
}
|
||||
|
||||
override init(_ evaluators: Evaluator...) {
|
||||
super.init()
|
||||
if num > 1 {
|
||||
self.evaluators.append(And(evaluators))
|
||||
} else { // 0 or 1
|
||||
self.evaluators.append(contentsOf: evaluators)
|
||||
}
|
||||
updateNumEvaluators()
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func add(_ evaluator: Evaluator) {
|
||||
evaluators.append(evaluator)
|
||||
updateNumEvaluators()
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ node: Element) -> Bool {
|
||||
for index in 0..<num {
|
||||
let evaluator: Evaluator = evaluators[index]
|
||||
do {
|
||||
if (try evaluator.matches(root, node)) {
|
||||
return true
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":or\(evaluators.map {String($0.toString())})"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+66
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Comment.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 22/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A comment node.
|
||||
*/
|
||||
public class Comment: Node {
|
||||
private static let COMMENT_KEY: String = "comment"
|
||||
|
||||
/**
|
||||
Create a new comment node.
|
||||
@param data The contents of the comment
|
||||
@param baseUri base URI
|
||||
*/
|
||||
public init(_ data: String, _ baseUri: String) {
|
||||
super.init(baseUri)
|
||||
do {
|
||||
try attributes?.put(Comment.COMMENT_KEY, data)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
public override func nodeName() -> String {
|
||||
return "#comment"
|
||||
}
|
||||
|
||||
/**
|
||||
Get the contents of the comment.
|
||||
@return comment content
|
||||
*/
|
||||
public func getData() -> String {
|
||||
return attributes!.get(key: Comment.COMMENT_KEY)
|
||||
}
|
||||
|
||||
override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {
|
||||
if (out.prettyPrint()) {
|
||||
indent(accum, depth, out)
|
||||
}
|
||||
accum
|
||||
.append("<!--")
|
||||
.append(getData())
|
||||
.append("-->")
|
||||
}
|
||||
|
||||
override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {}
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let clone = Comment(attributes!.get(key: Comment.COMMENT_KEY), baseUri!)
|
||||
return copy(clone: clone)
|
||||
}
|
||||
|
||||
public override func copy(parent: Node?) -> Node {
|
||||
let clone = Comment(attributes!.get(key: Comment.COMMENT_KEY), baseUri!)
|
||||
return copy(clone: clone, parent: parent)
|
||||
}
|
||||
|
||||
public override func copy(clone: Node, parent: Node?) -> Node {
|
||||
return super.copy(clone: clone, parent: parent)
|
||||
}
|
||||
}
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Connection.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
//TODO:
|
||||
Generated
+85
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// DataNode.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A data node, for contents of style, script tags etc, where contents should not show in text().
|
||||
*/
|
||||
open class DataNode: Node {
|
||||
private static let DATA_KEY: String = "data"
|
||||
|
||||
/**
|
||||
Create a new DataNode.
|
||||
@param data data contents
|
||||
@param baseUri base URI
|
||||
*/
|
||||
public init(_ data: String, _ baseUri: String) {
|
||||
super.init(baseUri)
|
||||
do {
|
||||
try attributes?.put(DataNode.DATA_KEY, data)
|
||||
} catch {}
|
||||
|
||||
}
|
||||
|
||||
open override func nodeName() -> String {
|
||||
return "#data"
|
||||
}
|
||||
|
||||
/**
|
||||
Get the data contents of this node. Will be unescaped and with original new lines, space etc.
|
||||
@return data
|
||||
*/
|
||||
open func getWholeData() -> String {
|
||||
return attributes!.get(key: DataNode.DATA_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data contents of this node.
|
||||
* @param data unencoded data
|
||||
* @return this node, for chaining
|
||||
*/
|
||||
@discardableResult
|
||||
open func setWholeData(_ data: String) -> DataNode {
|
||||
do {
|
||||
try attributes?.put(DataNode.DATA_KEY, data)
|
||||
} catch {}
|
||||
return self
|
||||
}
|
||||
|
||||
override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings)throws {
|
||||
accum.append(getWholeData()) // data is not escaped in return from data nodes, so " in script, style is plain
|
||||
}
|
||||
|
||||
override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {}
|
||||
|
||||
/**
|
||||
Create a new DataNode from HTML encoded data.
|
||||
@param encodedData encoded data
|
||||
@param baseUri bass URI
|
||||
@return new DataNode
|
||||
*/
|
||||
public static func createFromEncoded(_ encodedData: String, _ baseUri: String)throws->DataNode {
|
||||
let data = try Entities.unescape(encodedData)
|
||||
return DataNode(data, baseUri)
|
||||
}
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let clone = DataNode(attributes!.get(key: DataNode.DATA_KEY), baseUri!)
|
||||
return copy(clone: clone)
|
||||
}
|
||||
|
||||
public override func copy(parent: Node?) -> Node {
|
||||
let clone = DataNode(attributes!.get(key: DataNode.DATA_KEY), baseUri!)
|
||||
return copy(clone: clone, parent: parent)
|
||||
}
|
||||
|
||||
public override func copy(clone: Node, parent: Node?) -> Node {
|
||||
return super.copy(clone: clone, parent: parent)
|
||||
}
|
||||
}
|
||||
Generated
+24
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// DataUtil.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 02/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Internal static utilities for handling data.
|
||||
*
|
||||
*/
|
||||
class DataUtil {
|
||||
|
||||
static let charsetPattern = "(?i)\\bcharset=\\s*(?:\"|')?([^\\s,;\"']*)"
|
||||
static let defaultCharset = "UTF-8" // used if not found in header or meta charset
|
||||
static let bufferSize = 0x20000 // ~130K.
|
||||
static let UNICODE_BOM = 0xFEFF
|
||||
static let mimeBoundaryChars = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
static let boundaryLength = 32
|
||||
|
||||
}
|
||||
Generated
+562
@@ -0,0 +1,562 @@
|
||||
//
|
||||
// Document.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class Document: Element {
|
||||
public enum QuirksMode {
|
||||
case noQuirks, quirks, limitedQuirks
|
||||
}
|
||||
|
||||
private var _outputSettings: OutputSettings = OutputSettings()
|
||||
private var _quirksMode: Document.QuirksMode = QuirksMode.noQuirks
|
||||
private let _location: String
|
||||
private var updateMetaCharset: Bool = false
|
||||
|
||||
/**
|
||||
Create a new, empty Document.
|
||||
@param baseUri base URI of document
|
||||
@see org.jsoup.Jsoup#parse
|
||||
@see #createShell
|
||||
*/
|
||||
public init(_ baseUri: String) {
|
||||
self._location = baseUri
|
||||
super.init(try! Tag.valueOf("#root", ParseSettings.htmlDefault), baseUri)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a valid, empty shell of a document, suitable for adding more elements to.
|
||||
@param baseUri baseUri of document
|
||||
@return document with html, head, and body elements.
|
||||
*/
|
||||
static public func createShell(_ baseUri: String) -> Document {
|
||||
let doc: Document = Document(baseUri)
|
||||
let html: Element = try! doc.appendElement("html")
|
||||
try! html.appendElement("head")
|
||||
try! html.appendElement("body")
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL this Document was parsed from. If the starting URL is a redirect,
|
||||
* this will return the final URL from which the document was served from.
|
||||
* @return location
|
||||
*/
|
||||
public func location() -> String {
|
||||
return _location
|
||||
}
|
||||
|
||||
/**
|
||||
Accessor to the document's {@code head} element.
|
||||
@return {@code head}
|
||||
*/
|
||||
public func head() -> Element? {
|
||||
return findFirstElementByTagName("head", self)
|
||||
}
|
||||
|
||||
/**
|
||||
Accessor to the document's {@code body} element.
|
||||
@return {@code body}
|
||||
*/
|
||||
public func body() -> Element? {
|
||||
return findFirstElementByTagName("body", self)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the string contents of the document's {@code title} element.
|
||||
@return Trimmed title, or empty string if none set.
|
||||
*/
|
||||
public func title()throws->String {
|
||||
// title is a preserve whitespace tag (for document output), but normalised here
|
||||
let titleEl: Element? = try getElementsByTag("title").first()
|
||||
return titleEl != nil ? try StringUtil.normaliseWhitespace(titleEl!.text()).trim() : ""
|
||||
}
|
||||
|
||||
/**
|
||||
Set the document's {@code title} element. Updates the existing element, or adds {@code title} to {@code head} if
|
||||
not present
|
||||
@param title string to set as title
|
||||
*/
|
||||
public func title(_ title: String)throws {
|
||||
let titleEl: Element? = try getElementsByTag("title").first()
|
||||
if (titleEl == nil) { // add to head
|
||||
try head()?.appendElement("title").text(title)
|
||||
} else {
|
||||
try titleEl?.text(title)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Create a new Element, with this document's base uri. Does not make the new element a child of this document.
|
||||
@param tagName element tag name (e.g. {@code a})
|
||||
@return new element
|
||||
*/
|
||||
public func createElement(_ tagName: String)throws->Element {
|
||||
return try Element(Tag.valueOf(tagName, ParseSettings.preserveCase), self.getBaseUri())
|
||||
}
|
||||
|
||||
/**
|
||||
Normalise the document. This happens after the parse phase so generally does not need to be called.
|
||||
Moves any text content that is not in the body element into the body.
|
||||
@return this document after normalisation
|
||||
*/
|
||||
@discardableResult
|
||||
public func normalise()throws->Document {
|
||||
var htmlE: Element? = findFirstElementByTagName("html", self)
|
||||
if (htmlE == nil) {
|
||||
htmlE = try appendElement("html")
|
||||
}
|
||||
let htmlEl: Element = htmlE!
|
||||
|
||||
if (head() == nil) {
|
||||
try htmlEl.prependElement("head")
|
||||
}
|
||||
if (body() == nil) {
|
||||
try htmlEl.appendElement("body")
|
||||
}
|
||||
|
||||
// pull text nodes out of root, html, and head els, and push into body. non-text nodes are already taken care
|
||||
// of. do in inverse order to maintain text order.
|
||||
try normaliseTextNodes(head()!)
|
||||
try normaliseTextNodes(htmlEl)
|
||||
try normaliseTextNodes(self)
|
||||
|
||||
try normaliseStructure("head", htmlEl)
|
||||
try normaliseStructure("body", htmlEl)
|
||||
|
||||
try ensureMetaCharsetElement()
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// does not recurse.
|
||||
private func normaliseTextNodes(_ element: Element)throws {
|
||||
var toMove: Array<Node> = Array<Node>()
|
||||
for node: Node in element.childNodes {
|
||||
if let tn = (node as? TextNode) {
|
||||
if (!tn.isBlank()) {
|
||||
toMove.append(tn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in (0..<toMove.count).reversed() {
|
||||
let node: Node = toMove[i]
|
||||
try element.removeChild(node)
|
||||
try body()?.prependChild(TextNode(" ", ""))
|
||||
try body()?.prependChild(node)
|
||||
}
|
||||
}
|
||||
|
||||
// merge multiple <head> or <body> contents into one, delete the remainder, and ensure they are owned by <html>
|
||||
private func normaliseStructure(_ tag: String, _ htmlEl: Element)throws {
|
||||
let elements: Elements = try self.getElementsByTag(tag)
|
||||
let master: Element? = elements.first() // will always be available as created above if not existent
|
||||
if (elements.size() > 1) { // dupes, move contents to master
|
||||
var toMove: Array<Node> = Array<Node>()
|
||||
for i in 1..<elements.size() {
|
||||
let dupe: Node = elements.get(i)
|
||||
for node: Node in dupe.childNodes {
|
||||
toMove.append(node)
|
||||
}
|
||||
try dupe.remove()
|
||||
}
|
||||
|
||||
for dupe: Node in toMove {
|
||||
try master?.appendChild(dupe)
|
||||
}
|
||||
}
|
||||
// ensure parented by <html>
|
||||
if (!(master != nil && master!.parent() != nil && master!.parent()!.equals(htmlEl))) {
|
||||
try htmlEl.appendChild(master!) // includes remove()
|
||||
}
|
||||
}
|
||||
|
||||
// fast method to get first by tag name, used for html, head, body finders
|
||||
private func findFirstElementByTagName(_ tag: String, _ node: Node) -> Element? {
|
||||
if (node.nodeName()==tag) {
|
||||
return node as? Element
|
||||
} else {
|
||||
for child: Node in node.childNodes {
|
||||
let found: Element? = findFirstElementByTagName(tag, child)
|
||||
if (found != nil) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
open override func outerHtml()throws->String {
|
||||
return try super.html() // no outer wrapper tag
|
||||
}
|
||||
|
||||
/**
|
||||
Set the text of the {@code body} of this document. Any existing nodes within the body will be cleared.
|
||||
@param text unencoded text
|
||||
@return this document
|
||||
*/
|
||||
@discardableResult
|
||||
public override func text(_ text: String)throws->Element {
|
||||
try body()?.text(text) // overridden to not nuke doc structure
|
||||
return self
|
||||
}
|
||||
|
||||
open override func nodeName() -> String {
|
||||
return "#document"
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the charset used in this document. This method is equivalent
|
||||
* to {@link OutputSettings#charset(java.nio.charset.Charset)
|
||||
* OutputSettings.charset(Charset)} but in addition it updates the
|
||||
* charset / encoding element within the document.
|
||||
*
|
||||
* <p>This enables
|
||||
* {@link #updateMetaCharsetElement(boolean) meta charset update}.</p>
|
||||
*
|
||||
* <p>If there's no element with charset / encoding information yet it will
|
||||
* be created. Obsolete charset / encoding definitions are removed!</p>
|
||||
*
|
||||
* <p><b>Elements used:</b></p>
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>Html:</b> <i><meta charset="CHARSET"></i></li>
|
||||
* <li><b>Xml:</b> <i><?xml version="1.0" encoding="CHARSET"></i></li>
|
||||
* </ul>
|
||||
*
|
||||
* @param charset Charset
|
||||
*
|
||||
* @see #updateMetaCharsetElement(boolean)
|
||||
* @see OutputSettings#charset(java.nio.charset.Charset)
|
||||
*/
|
||||
public func charset(_ charset: String.Encoding)throws {
|
||||
updateMetaCharsetElement(true)
|
||||
_outputSettings.charset(charset)
|
||||
try ensureMetaCharsetElement()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the charset used in this document. This method is equivalent
|
||||
* to {@link OutputSettings#charset()}.
|
||||
*
|
||||
* @return Current Charset
|
||||
*
|
||||
* @see OutputSettings#charset()
|
||||
*/
|
||||
public func charset()->String.Encoding {
|
||||
return _outputSettings.charset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets 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.
|
||||
*
|
||||
* <p>If set to <tt>false</tt> <i>(default)</i> there are no elements
|
||||
* modified.</p>
|
||||
*
|
||||
* @param update If <tt>true</tt> the element updated on charset
|
||||
* changes, <tt>false</tt> 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 <tt>true</tt> if the element is updated on charset
|
||||
* changes, <tt>false</tt> 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
|
||||
* <tt>true</tt>, otherwise this method does nothing.
|
||||
*
|
||||
* <ul>
|
||||
* <li>An exsiting element gets updated with the current charset</li>
|
||||
* <li>If there's no element yet it will be inserted</li>
|
||||
* <li>Obsolete elements are removed</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Elements used:</b></p>
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>Html:</b> <i><meta charset="CHARSET"></i></li>
|
||||
* <li><b>Xml:</b> <i><?xml version="1.0" encoding="CHARSET"></i></li>
|
||||
* </ul>
|
||||
*/
|
||||
private func ensureMetaCharsetElement()throws {
|
||||
if (updateMetaCharset) {
|
||||
let syntax: OutputSettings.Syntax = outputSettings().syntax()
|
||||
|
||||
if (syntax == OutputSettings.Syntax.html) {
|
||||
let metaCharset: Element? = try select("meta[charset]").first()
|
||||
|
||||
if (metaCharset != nil) {
|
||||
try metaCharset?.attr("charset", charset().displayName())
|
||||
} else {
|
||||
let head: Element? = self.head()
|
||||
|
||||
if (head != nil) {
|
||||
try head?.appendElement("meta").attr("charset", charset().displayName())
|
||||
}
|
||||
}
|
||||
|
||||
// Remove obsolete elements
|
||||
let s = try select("meta[name=charset]")
|
||||
try s.remove()
|
||||
|
||||
} else if (syntax == OutputSettings.Syntax.xml) {
|
||||
let node: Node = getChildNodes()[0]
|
||||
|
||||
if let decl = (node as? XmlDeclaration) {
|
||||
|
||||
if (decl.name()=="xml") {
|
||||
try decl.attr("encoding", charset().displayName())
|
||||
|
||||
_ = try decl.attr("version")
|
||||
try decl.attr("version", "1.0")
|
||||
} else {
|
||||
try Validate.notNull(obj: baseUri)
|
||||
let decl = XmlDeclaration("xml", baseUri!, false)
|
||||
try decl.attr("version", "1.0")
|
||||
try decl.attr("encoding", charset().displayName())
|
||||
|
||||
try prependChild(decl)
|
||||
}
|
||||
} else {
|
||||
try Validate.notNull(obj: baseUri)
|
||||
let decl = XmlDeclaration("xml", baseUri!, false)
|
||||
try decl.attr("version", "1.0")
|
||||
try decl.attr("encoding", charset().displayName())
|
||||
|
||||
try prependChild(decl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the document's current output settings.
|
||||
* @return the document's current output settings.
|
||||
*/
|
||||
public func outputSettings() -> OutputSettings {
|
||||
return _outputSettings
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the document's output settings.
|
||||
* @param outputSettings new output settings.
|
||||
* @return this document, for chaining.
|
||||
*/
|
||||
@discardableResult
|
||||
public func outputSettings(_ outputSettings: OutputSettings) -> Document {
|
||||
self._outputSettings = outputSettings
|
||||
return self
|
||||
}
|
||||
|
||||
public func quirksMode()->Document.QuirksMode {
|
||||
return _quirksMode
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func quirksMode(_ quirksMode: Document.QuirksMode) -> Document {
|
||||
self._quirksMode = quirksMode
|
||||
return self
|
||||
}
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let clone = Document(_location)
|
||||
return copy(clone: clone)
|
||||
}
|
||||
|
||||
public override func copy(parent: Node?) -> Node {
|
||||
let clone = Document(_location)
|
||||
return copy(clone: clone, parent: parent)
|
||||
}
|
||||
|
||||
public override func copy(clone: Node, parent: Node?) -> Node {
|
||||
let clone = clone as! Document
|
||||
clone._outputSettings = _outputSettings.copy() as! OutputSettings
|
||||
clone._quirksMode = _quirksMode
|
||||
clone.updateMetaCharset = updateMetaCharset
|
||||
return super.copy(clone: clone, parent: parent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class OutputSettings: NSCopying {
|
||||
/**
|
||||
* The output serialization syntax.
|
||||
*/
|
||||
public enum Syntax {case html, xml}
|
||||
|
||||
private var _escapeMode: Entities.EscapeMode = Entities.EscapeMode.base
|
||||
private var _encoder: String.Encoding = String.Encoding.utf8 // Charset.forName("UTF-8")
|
||||
private var _prettyPrint: Bool = true
|
||||
private var _outline: Bool = false
|
||||
private var _indentAmount: UInt = 1
|
||||
private var _syntax = Syntax.html
|
||||
|
||||
public init() {}
|
||||
|
||||
/**
|
||||
* Get the document's current HTML escape mode: <code>base</code>, which provides a limited set of named HTML
|
||||
* entities and escapes other characters as numbered entities for maximum compatibility; or <code>extended</code>,
|
||||
* which uses the complete set of HTML named entities.
|
||||
* <p>
|
||||
* The default escape mode is <code>base</code>.
|
||||
* @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 <code>html()</code> methods), and which are kept intact.
|
||||
* <p>
|
||||
* 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
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -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 <!DOCTYPE>} 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("<!doctype")
|
||||
} else {
|
||||
accum.append("<!DOCTYPE")
|
||||
}
|
||||
if (has(DocumentType.NAME)) {
|
||||
do {
|
||||
accum.append(" ").append(try attr(DocumentType.NAME))
|
||||
} catch {}
|
||||
|
||||
}
|
||||
|
||||
if (has(DocumentType.PUB_SYS_KEY)) {
|
||||
do {
|
||||
try accum.append(" ").append(attr(DocumentType.PUB_SYS_KEY))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (has(DocumentType.PUBLIC_ID)) {
|
||||
do {
|
||||
try accum.append(" \"").append(attr(DocumentType.PUBLIC_ID)).append("\"")
|
||||
} catch {}
|
||||
|
||||
}
|
||||
if (has(DocumentType.SYSTEM_ID)) {
|
||||
do {
|
||||
accum.append(" \"").append(try attr(DocumentType.SYSTEM_ID)).append("\"")
|
||||
} catch {}
|
||||
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+1295
File diff suppressed because it is too large
Load Diff
Generated
+616
@@ -0,0 +1,616 @@
|
||||
//
|
||||
// Elements.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 20/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
/**
|
||||
A list of {@link Element}s, with methods that act on every element in the list.
|
||||
<p>
|
||||
To get an {@code Elements} object, use the {@link Element#select(String)} method.
|
||||
</p>
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
//open typealias Elements = Array<Element>
|
||||
//typealias E = Element
|
||||
open class Elements: NSCopying {
|
||||
fileprivate var this: Array<Element> = Array<Element>()
|
||||
|
||||
///base init
|
||||
public init() {
|
||||
}
|
||||
///Initialized with an array
|
||||
public init(_ a: Array<Element>) {
|
||||
this = a
|
||||
}
|
||||
///Initialized with an order set
|
||||
public init(_ a: OrderedSet<Element>) {
|
||||
this.append(contentsOf: a)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deep copy of these elements.
|
||||
* @return a deep copy
|
||||
*/
|
||||
public func copy(with zone: NSZone? = nil) -> Any {
|
||||
let clone: Elements = Elements()
|
||||
for e: Element in this {
|
||||
clone.add(e.copy() as! Element)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// attribute methods
|
||||
/**
|
||||
Get an attribute value from the first matched element that has the attribute.
|
||||
@param attributeKey The attribute key.
|
||||
@return The attribute value from the first matched element that has the attribute.. If no elements were matched (isEmpty() == true),
|
||||
or if the no elements have the attribute, returns empty string.
|
||||
@see #hasAttr(String)
|
||||
*/
|
||||
open func attr(_ attributeKey: String)throws->String {
|
||||
for element in this {
|
||||
if (element.hasAttr(attributeKey)) {
|
||||
return try element.attr(attributeKey)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if any of the matched elements have this attribute set.
|
||||
@param attributeKey attribute key
|
||||
@return true if any of the elements have the attribute; false if none do.
|
||||
*/
|
||||
open func hasAttr(_ attributeKey: String) -> Bool {
|
||||
for element in this {
|
||||
if element.hasAttr(attributeKey) {return true}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an attribute on all matched elements.
|
||||
* @param attributeKey attribute key
|
||||
* @param attributeValue attribute value
|
||||
* @return this
|
||||
*/
|
||||
@discardableResult
|
||||
open func attr(_ attributeKey: String, _ attributeValue: String)throws->Elements {
|
||||
for element in this {
|
||||
try element.attr(attributeKey, attributeValue)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an attribute from every matched element.
|
||||
* @param attributeKey The attribute to remove.
|
||||
* @return this (for chaining)
|
||||
*/
|
||||
@discardableResult
|
||||
open func removeAttr(_ attributeKey: String)throws->Elements {
|
||||
for element in this {
|
||||
try element.removeAttr(attributeKey)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Add the class name to every matched element's {@code class} attribute.
|
||||
@param className class name to add
|
||||
@return this
|
||||
*/
|
||||
@discardableResult
|
||||
open func addClass(_ className: String)throws->Elements {
|
||||
for element in this {
|
||||
try element.addClass(className)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Remove the class name from every matched element's {@code class} attribute, if present.
|
||||
@param className class name to remove
|
||||
@return this
|
||||
*/
|
||||
@discardableResult
|
||||
open func removeClass(_ className: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.removeClass(className)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Toggle the class name on every matched element's {@code class} attribute.
|
||||
@param className class name to add if missing, or remove if present, from every element.
|
||||
@return this
|
||||
*/
|
||||
@discardableResult
|
||||
open func toggleClass(_ className: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.toggleClass(className)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Determine if any of the matched elements have this class name set in their {@code class} attribute.
|
||||
@param className class name to check for
|
||||
@return true if any do, false if none do
|
||||
*/
|
||||
|
||||
open func hasClass(_ className: String) -> Bool {
|
||||
for element: Element in this {
|
||||
if (element.hasClass(className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form element's value of the first matched element.
|
||||
* @return The form element's value, or empty if not set.
|
||||
* @see Element#val()
|
||||
*/
|
||||
open func val()throws->String {
|
||||
if (size() > 0) {
|
||||
return try first()!.val()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the form element's value in each of the matched elements.
|
||||
* @param value The value to set into each matched element
|
||||
* @return this (for chaining)
|
||||
*/
|
||||
@discardableResult
|
||||
open func val(_ value: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.val(value)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the combined text of all the matched elements.
|
||||
* <p>
|
||||
* 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()throws->String {
|
||||
let sb: StringBuilder = StringBuilder()
|
||||
for element: Element in this {
|
||||
if (sb.length != 0) {
|
||||
sb.append(" ")
|
||||
}
|
||||
sb.append(try element.text())
|
||||
}
|
||||
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 combined inner HTML of all matched elements.
|
||||
* @return string of all element's inner HTML.
|
||||
* @see #text()
|
||||
* @see #outerHtml()
|
||||
*/
|
||||
open func html()throws->String {
|
||||
let sb: StringBuilder = StringBuilder()
|
||||
for element: Element in this {
|
||||
if (sb.length != 0) {
|
||||
sb.append("\n")
|
||||
}
|
||||
sb.append(try element.html())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the combined outer HTML of all matched elements.
|
||||
* @return string of all element's outer HTML.
|
||||
* @see #text()
|
||||
* @see #html()
|
||||
*/
|
||||
open func outerHtml()throws->String {
|
||||
let sb: StringBuilder = StringBuilder()
|
||||
for element in this {
|
||||
if (sb.length != 0) {
|
||||
sb.append("\n")
|
||||
}
|
||||
sb.append(try element.outerHtml())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the combined outer HTML of all matched elements. Alias of {@link #outerHtml()}.
|
||||
* @return string of all element's outer HTML.
|
||||
* @see #text()
|
||||
* @see #html()
|
||||
*/
|
||||
|
||||
open func toString()throws->String {
|
||||
return try outerHtml()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tag name of each matched element. For example, to change each {@code <i>} to a {@code <em>}, do
|
||||
* {@code doc.select("i").tagName("em");}
|
||||
* @param tagName the new tag name
|
||||
* @return this, for chaining
|
||||
* @see Element#tagName(String)
|
||||
*/
|
||||
@discardableResult
|
||||
open func tagName(_ tagName: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.tagName(tagName)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inner HTML of each matched element.
|
||||
* @param html HTML to parse and set into each matched element.
|
||||
* @return this, for chaining
|
||||
* @see Element#html(String)
|
||||
*/
|
||||
@discardableResult
|
||||
open func html(_ html: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.html(html)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the supplied HTML to the start of each matched element's inner HTML.
|
||||
* @param html HTML to add inside each element, before the existing HTML
|
||||
* @return this, for chaining
|
||||
* @see Element#prepend(String)
|
||||
*/
|
||||
@discardableResult
|
||||
open func prepend(_ html: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.prepend(html)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the supplied HTML to the end of each matched element's inner HTML.
|
||||
* @param html HTML to add inside each element, after the existing HTML
|
||||
* @return this, for chaining
|
||||
* @see Element#append(String)
|
||||
*/
|
||||
@discardableResult
|
||||
open func append(_ html: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.append(html)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the supplied HTML before each matched element's outer HTML.
|
||||
* @param html HTML to insert before each element
|
||||
* @return this, for chaining
|
||||
* @see Element#before(String)
|
||||
*/
|
||||
@discardableResult
|
||||
open func before(_ html: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.before(html)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the supplied HTML after each matched element's outer HTML.
|
||||
* @param html HTML to insert after each element
|
||||
* @return this, for chaining
|
||||
* @see Element#after(String)
|
||||
*/
|
||||
@discardableResult
|
||||
open func after(_ html: String)throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.after(html)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Wrap the supplied HTML around each matched elements. For example, with HTML
|
||||
{@code <p><b>This</b> is <b>Jsoup</b></p>},
|
||||
<code>doc.select("b").wrap("<i></i>");</code>
|
||||
becomes {@code <p><i><b>This</b></i> is <i><b>jsoup</b></i></p>}
|
||||
@param html HTML to wrap around each element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
|
||||
@return this (for chaining)
|
||||
@see Element#wrap
|
||||
*/
|
||||
@discardableResult
|
||||
open func wrap(_ html: String)throws->Elements {
|
||||
try Validate.notEmpty(string: html)
|
||||
for element: Element in this {
|
||||
try element.wrap(html)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the matched elements from the DOM, and moves their children up into their parents. This has the effect of
|
||||
* dropping the elements but keeping their children.
|
||||
* <p>
|
||||
* This is useful for e.g removing unwanted formatting elements but keeping their contents.
|
||||
* </p>
|
||||
*
|
||||
* E.g. with HTML: <p>{@code <div><font>One</font> <font><a href="/">Two</a></font></div>}</p>
|
||||
* <p>{@code doc.select("font").unwrap();}</p>
|
||||
* <p>HTML = {@code <div>One <a href="/">Two</a></div>}</p>
|
||||
*
|
||||
* @return this (for chaining)
|
||||
* @see Node#unwrap
|
||||
*/
|
||||
@discardableResult
|
||||
open func unwrap()throws->Elements {
|
||||
for element: Element in this {
|
||||
try element.unwrap()
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty (remove all child nodes from) each matched element. This is similar to setting the inner HTML of each
|
||||
* element to nothing.
|
||||
* <p>
|
||||
* E.g. HTML: {@code <div><p>Hello <b>there</b></p> <p>now</p></div>}<br>
|
||||
* <code>doc.select("p").empty();</code><br>
|
||||
* HTML = {@code <div><p></p> <p></p></div>}
|
||||
* @return this, for chaining
|
||||
* @see Element#empty()
|
||||
* @see #remove()
|
||||
*/
|
||||
@discardableResult
|
||||
open func empty() -> Elements {
|
||||
for element: Element in this {
|
||||
element.empty()
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove each matched element from the DOM. This is similar to setting the outer HTML of each element to nothing.
|
||||
* <p>
|
||||
* E.g. HTML: {@code <div><p>Hello</p> <p>there</p> <img /></div>}<br>
|
||||
* <code>doc.select("p").remove();</code><br>
|
||||
* HTML = {@code <div> <img /></div>}
|
||||
* <p>
|
||||
* Note that this method should not be used to clean user-submitted HTML; rather, use {@link org.jsoup.safety.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 Selector} query
|
||||
* @return the filtered list of elements, or an empty list if none match.
|
||||
*/
|
||||
open func select(_ query: String)throws->Elements {
|
||||
return try Selector.select(query, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove elements from this list that match the {@link Selector} query.
|
||||
* <p>
|
||||
* E.g. HTML: {@code <div class=logo>One</div> <div>Two</div>}<br>
|
||||
* <code>Elements divs = doc.select("div").not(".logo");</code><br>
|
||||
* Result: {@code divs: [<div>Two</div>]}
|
||||
* <p>
|
||||
* @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 Selector.select(query, this)
|
||||
return Selector.filterOut(this, out.this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <i>nth</i> matched element as an Elements object.
|
||||
* <p>
|
||||
* 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<Element> = OrderedSet<Element>()
|
||||
for e: Element in this {
|
||||
combo.append(contentsOf: e.parents().array())
|
||||
}
|
||||
return Elements(combo)
|
||||
}
|
||||
|
||||
// list-like methods
|
||||
/**
|
||||
Get the first matched element.
|
||||
@return The first matched element, or <code>null</code> 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 <code>null</code> 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<FormElement> {
|
||||
var forms: Array<FormElement> = Array<FormElement>()
|
||||
for el: Element in this {
|
||||
if let el = el as? FormElement {
|
||||
forms.append(el)
|
||||
}
|
||||
}
|
||||
return forms
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the specified element to the end of this list.
|
||||
*
|
||||
* @param e element to be appended to this list
|
||||
* @return <tt>true</tt> (as specified by {@link Collection#add})
|
||||
*/
|
||||
open func add(_ e: Element) {
|
||||
this.append(e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the specified element at index.
|
||||
*/
|
||||
open func add(_ index: Int, _ element: Element) {
|
||||
this.insert(element, at: index)
|
||||
}
|
||||
|
||||
/// Return element at index
|
||||
open func get(_ i: Int) -> Element {
|
||||
return this[i]
|
||||
}
|
||||
|
||||
/// Returns all elements
|
||||
open func array()->Array<Element> {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elements extension Equatable.
|
||||
*/
|
||||
extension Elements: Equatable {
|
||||
/// 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: Elements, rhs: Elements) -> Bool {
|
||||
return lhs.this == rhs.this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elements IteratorProtocol.
|
||||
*/
|
||||
public struct ElementsIterator: IteratorProtocol {
|
||||
/// Elements reference
|
||||
let elements: Elements
|
||||
//current element index
|
||||
var index = 0
|
||||
|
||||
/// Initializer
|
||||
init(_ countdown: Elements) {
|
||||
self.elements = countdown
|
||||
}
|
||||
|
||||
/// Advances to the next element and returns it, or `nil` if no next element
|
||||
mutating public func next() -> Element? {
|
||||
let result = index < elements.size() ? elements.get(index) : nil
|
||||
index += 1
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elements Extension Sequence.
|
||||
*/
|
||||
extension Elements: Sequence {
|
||||
/// Returns an iterator over the elements of this sequence.
|
||||
public func makeIterator() -> ElementsIterator {
|
||||
return ElementsIterator(self)
|
||||
}
|
||||
}
|
||||
Generated
+382
File diff suppressed because one or more lines are too long
Generated
+720
@@ -0,0 +1,720 @@
|
||||
//
|
||||
// Evaluator.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 22/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Evaluates that an element matches the selector.
|
||||
*/
|
||||
public class Evaluator {
|
||||
init () {}
|
||||
|
||||
/**
|
||||
* Test if the element meets the evaluator's requirements.
|
||||
*
|
||||
* @param root Root of the matching subtree
|
||||
* @param element tested element
|
||||
* @return Returns <tt>true</tt> if the requirements are met or
|
||||
* <tt>false</tt> otherwise
|
||||
*/
|
||||
open func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
preconditionFailure("self method must be overridden")
|
||||
}
|
||||
|
||||
open func toString() -> String {
|
||||
preconditionFailure("self method must be overridden")
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for tag name
|
||||
*/
|
||||
public class Tag: Evaluator {
|
||||
private let tagName: String
|
||||
private let tagNameNormal: String
|
||||
|
||||
public init(_ tagName: String) {
|
||||
self.tagName = tagName
|
||||
self.tagNameNormal = tagName.lowercased()
|
||||
}
|
||||
|
||||
open override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return element.tagNameNormal() == tagNameNormal
|
||||
}
|
||||
|
||||
open override func toString() -> String {
|
||||
return String(tagName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for tag name that ends with
|
||||
*/
|
||||
public final class TagEndsWith: Evaluator {
|
||||
private let tagName: String
|
||||
|
||||
public init(_ tagName: String) {
|
||||
self.tagName = tagName
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return (element.tagName().hasSuffix(tagName))
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return String(tagName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for element id
|
||||
*/
|
||||
public final class Id: Evaluator {
|
||||
private let id: String
|
||||
|
||||
public init(_ id: String) {
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return (id == element.id())
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "#\(id)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for element class
|
||||
*/
|
||||
public final class Class: Evaluator {
|
||||
private let className: String
|
||||
|
||||
public init(_ className: String) {
|
||||
self.className = className
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element) -> Bool {
|
||||
return (element.hasClass(className))
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ".\(className)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for attribute name matching
|
||||
*/
|
||||
public final class Attribute: Evaluator {
|
||||
private let key: String
|
||||
|
||||
public init(_ key: String) {
|
||||
self.key = key
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return element.hasAttr(key)
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "[\(key)]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for attribute name prefix matching
|
||||
*/
|
||||
public final class AttributeStarting: Evaluator {
|
||||
private let keyPrefix: String
|
||||
|
||||
public init(_ keyPrefix: String)throws {
|
||||
try Validate.notEmpty(string: keyPrefix)
|
||||
self.keyPrefix = keyPrefix.lowercased()
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
if let values = element.getAttributes() {
|
||||
for attribute in values where attribute.getKey().lowercased().hasPrefix(keyPrefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "[^\(keyPrefix)]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for attribute name/value matching
|
||||
*/
|
||||
public final class AttributeWithValue: AttributeKeyPair {
|
||||
public override init(_ key: String, _ value: String)throws {
|
||||
try super.init(key, value)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
if element.hasAttr(key) {
|
||||
let string = try element.attr(key)
|
||||
return value.equalsIgnoreCase(string: string.trim())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "[\(key)=\(value)]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for attribute name != value matching
|
||||
*/
|
||||
public final class AttributeWithValueNot: AttributeKeyPair {
|
||||
public override init(_ key: String, _ value: String)throws {
|
||||
try super.init(key, value)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let string = try element.attr(key)
|
||||
return !value.equalsIgnoreCase(string: string)
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "[\(key)!=\(value)]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for attribute name/value matching (value prefix)
|
||||
*/
|
||||
public final class AttributeWithValueStarting: AttributeKeyPair {
|
||||
public override init(_ key: String, _ value: String)throws {
|
||||
try super.init(key, value)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
if element.hasAttr(key) {
|
||||
return try element.attr(key).lowercased().hasPrefix(value) // value is lower case already
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "[\(key)^=\(value)]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for attribute name/value matching (value ending)
|
||||
*/
|
||||
public final class AttributeWithValueEnding: AttributeKeyPair {
|
||||
public override init(_ key: String, _ value: String)throws {
|
||||
try super.init(key, value)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
if element.hasAttr(key) {
|
||||
return try element.attr(key).lowercased().hasSuffix(value) // value is lower case
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "[\(key)$=\(value)]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for attribute name/value matching (value containing)
|
||||
*/
|
||||
public final class AttributeWithValueContaining: AttributeKeyPair {
|
||||
public override init(_ key: String, _ value: String)throws {
|
||||
try super.init(key, value)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
if element.hasAttr(key) {
|
||||
return try element.attr(key).lowercased().contains(value) // value is lower case
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "[\(key)*=\(value)]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for attribute name/value matching (value regex matching)
|
||||
*/
|
||||
public final class AttributeWithValueMatching: Evaluator {
|
||||
let key: String
|
||||
let pattern: Pattern
|
||||
|
||||
public init(_ key: String, _ pattern: Pattern) {
|
||||
self.key = key.trim().lowercased()
|
||||
self.pattern = pattern
|
||||
super.init()
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
if element.hasAttr(key) {
|
||||
let string = try element.attr(key)
|
||||
return pattern.matcher(in: string).find()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "[\(key)~=\(pattern.toString())]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract evaluator for attribute name/value matching
|
||||
*/
|
||||
public class AttributeKeyPair: Evaluator {
|
||||
let key: String
|
||||
var value: String
|
||||
|
||||
public init(_ key: String, _ value2: String)throws {
|
||||
var value2 = value2
|
||||
try Validate.notEmpty(string: key)
|
||||
try Validate.notEmpty(string: value2)
|
||||
|
||||
self.key = key.trim().lowercased()
|
||||
if value2.startsWith("\"") && value2.hasSuffix("\"") || value2.startsWith("'") && value2.hasSuffix("'") {
|
||||
value2 = value2.substring(1, value2.count-2)
|
||||
}
|
||||
self.value = value2.trim().lowercased()
|
||||
}
|
||||
|
||||
open override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
preconditionFailure("self method must be overridden")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for any / all element matching
|
||||
*/
|
||||
public final class AllElements: Evaluator {
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return "*"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching by sibling index number (e {@literal <} idx)
|
||||
*/
|
||||
public final class IndexLessThan: IndexEvaluator {
|
||||
public override init(_ index: Int) {
|
||||
super.init(index)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return try element.elementSiblingIndex() < index
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":lt(\(index))"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching by sibling index number (e {@literal >} idx)
|
||||
*/
|
||||
public final class IndexGreaterThan: IndexEvaluator {
|
||||
public override init(_ index: Int) {
|
||||
super.init(index)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return try element.elementSiblingIndex() > index
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":gt(\(index))"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching by sibling index number (e = idx)
|
||||
*/
|
||||
public final class IndexEquals: IndexEvaluator {
|
||||
public override init(_ index: Int) {
|
||||
super.init(index)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return try element.elementSiblingIndex() == index
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":eq(\(index))"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching the last sibling (css :last-child)
|
||||
*/
|
||||
public final class IsLastChild: Evaluator {
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
|
||||
if let parent = element.parent() {
|
||||
let index = try element.elementSiblingIndex()
|
||||
return !(parent is Document) && index == (parent.getChildNodes().count - 1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":last-child"
|
||||
}
|
||||
}
|
||||
|
||||
public final class IsFirstOfType: IsNthOfType {
|
||||
public init() {
|
||||
super.init(0, 1)
|
||||
}
|
||||
public override func toString() -> String {
|
||||
return ":first-of-type"
|
||||
}
|
||||
}
|
||||
|
||||
public final class IsLastOfType: IsNthLastOfType {
|
||||
public init() {
|
||||
super.init(0, 1)
|
||||
}
|
||||
public override func toString() -> String {
|
||||
return ":last-of-type"
|
||||
}
|
||||
}
|
||||
|
||||
public class CssNthEvaluator: Evaluator {
|
||||
public let a: Int
|
||||
public let b: Int
|
||||
|
||||
public init(_ a: Int, _ b: Int) {
|
||||
self.a = a
|
||||
self.b = b
|
||||
}
|
||||
public init(_ b: Int) {
|
||||
self.a = 0
|
||||
self.b = b
|
||||
}
|
||||
|
||||
open override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let p: Element? = element.parent()
|
||||
if (p == nil || (((p as? Document) != nil))) {return false}
|
||||
|
||||
let pos: Int = try calculatePosition(root, element)
|
||||
if (a == 0) {return pos == b}
|
||||
|
||||
return (pos-b)*a >= 0 && (pos-b)%a==0
|
||||
}
|
||||
|
||||
open override func toString() -> String {
|
||||
if (a == 0) {
|
||||
return ":\(getPseudoClass)(\(b))"
|
||||
}
|
||||
if (b == 0) {
|
||||
return ":\(getPseudoClass)(\(a))"
|
||||
}
|
||||
return ":\(getPseudoClass)(\(a)\(b))"
|
||||
}
|
||||
|
||||
open func getPseudoClass() -> String {
|
||||
preconditionFailure("self method must be overridden")
|
||||
}
|
||||
open func calculatePosition(_ root: Element, _ element: Element)throws->Int {
|
||||
preconditionFailure("self method must be overridden")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* css-compatible Evaluator for :eq (css :nth-child)
|
||||
*
|
||||
* @see IndexEquals
|
||||
*/
|
||||
public final class IsNthChild: CssNthEvaluator {
|
||||
|
||||
public override init(_ a: Int, _ b: Int) {
|
||||
super.init(a, b)
|
||||
}
|
||||
|
||||
public override func calculatePosition(_ root: Element, _ element: Element)throws->Int {
|
||||
return try element.elementSiblingIndex()+1
|
||||
}
|
||||
|
||||
public override func getPseudoClass() -> String {
|
||||
return "nth-child"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* css pseudo class :nth-last-child)
|
||||
*
|
||||
* @see IndexEquals
|
||||
*/
|
||||
public final class IsNthLastChild: CssNthEvaluator {
|
||||
public override init(_ a: Int, _ b: Int) {
|
||||
super.init(a, b)
|
||||
}
|
||||
|
||||
public override func calculatePosition(_ root: Element, _ element: Element)throws->Int {
|
||||
var i = 0
|
||||
|
||||
if let l = element.parent() {
|
||||
i = l.children().array().count
|
||||
}
|
||||
return i - (try element.elementSiblingIndex())
|
||||
}
|
||||
|
||||
public override func getPseudoClass() -> String {
|
||||
return "nth-last-child"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* css pseudo class nth-of-type
|
||||
*
|
||||
*/
|
||||
public class IsNthOfType: CssNthEvaluator {
|
||||
public override init(_ a: Int, _ b: Int) {
|
||||
super.init(a, b)
|
||||
}
|
||||
|
||||
open override func calculatePosition(_ root: Element, _ element: Element) -> Int {
|
||||
var pos = 0
|
||||
let family: Elements? = element.parent()?.children()
|
||||
if let array = family?.array() {
|
||||
for el in array {
|
||||
if (el.tag() == element.tag()) {pos+=1}
|
||||
if (el === element) {break}
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
open override func getPseudoClass() -> String {
|
||||
return "nth-of-type"
|
||||
}
|
||||
}
|
||||
|
||||
public class IsNthLastOfType: CssNthEvaluator {
|
||||
|
||||
public override init(_ a: Int, _ b: Int) {
|
||||
super.init(a, b)
|
||||
}
|
||||
|
||||
open override func calculatePosition(_ root: Element, _ element: Element)throws->Int {
|
||||
var pos = 0
|
||||
if let family = element.parent()?.children() {
|
||||
let x = try element.elementSiblingIndex()
|
||||
for i in x..<family.array().count {
|
||||
if (family.get(i).tag() == element.tag()) {
|
||||
pos+=1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
open override func getPseudoClass() -> String {
|
||||
return "nth-last-of-type"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching the first sibling (css :first-child)
|
||||
*/
|
||||
public final class IsFirstChild: Evaluator {
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let p = element.parent()
|
||||
if(p != nil && !(((p as? Document) != nil))) {
|
||||
return (try element.elementSiblingIndex()) == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":first-child"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* css3 pseudo-class :root
|
||||
* @see <a href="http://www.w3.org/TR/selectors/#root-pseudo">:root selector</a>
|
||||
*
|
||||
*/
|
||||
public final class IsRoot: Evaluator {
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let r: Element = ((root as? Document) != nil) ? root.child(0) : root
|
||||
return element === r
|
||||
}
|
||||
public override func toString() -> String {
|
||||
return ":root"
|
||||
}
|
||||
}
|
||||
|
||||
public final class IsOnlyChild: Evaluator {
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let p = element.parent()
|
||||
return p != nil && !((p as? Document) != nil) && element.siblingElements().array().count == 0
|
||||
}
|
||||
public override func toString() -> String {
|
||||
return ":only-child"
|
||||
}
|
||||
}
|
||||
|
||||
public final class IsOnlyOfType: Evaluator {
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let p = element.parent()
|
||||
if (p == nil || (p as? Document) != nil) {return false}
|
||||
|
||||
var pos = 0
|
||||
if let family = p?.children().array() {
|
||||
for el in family {
|
||||
if (el.tag() == element.tag()) {pos+=1}
|
||||
}
|
||||
}
|
||||
return pos == 1
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":only-of-type"
|
||||
}
|
||||
}
|
||||
|
||||
public final class IsEmpty: Evaluator {
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let family: Array<Node> = element.getChildNodes()
|
||||
for n in family {
|
||||
if (!((n as? Comment) != nil || (n as? XmlDeclaration) != nil || (n as? DocumentType) != nil)) {return false}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":empty"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract evaluator for sibling index matching
|
||||
*
|
||||
* @author ant
|
||||
*/
|
||||
public class IndexEvaluator: Evaluator {
|
||||
let index: Int
|
||||
|
||||
public init(_ index: Int) {
|
||||
self.index = index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching Element (and its descendants) text
|
||||
*/
|
||||
public final class ContainsText: Evaluator {
|
||||
private let searchText: String
|
||||
|
||||
public init(_ searchText: String) {
|
||||
self.searchText = searchText.lowercased()
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return (try element.text().lowercased().contains(searchText))
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":contains(\(searchText)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching Element's own text
|
||||
*/
|
||||
public final class ContainsOwnText: Evaluator {
|
||||
private let searchText: String
|
||||
|
||||
public init(_ searchText: String) {
|
||||
self.searchText = searchText.lowercased()
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
return (element.ownText().lowercased().contains(searchText))
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":containsOwn(\(searchText)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching Element (and its descendants) text with regex
|
||||
*/
|
||||
public final class Matches: Evaluator {
|
||||
private let pattern: Pattern
|
||||
|
||||
public init(_ pattern: Pattern) {
|
||||
self.pattern = pattern
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let m = try pattern.matcher(in: element.text())
|
||||
return m.find()
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":matches(\(pattern)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluator for matching Element's own text with regex
|
||||
*/
|
||||
public final class MatchesOwn: Evaluator {
|
||||
private let pattern: Pattern
|
||||
|
||||
public init(_ pattern: Pattern) {
|
||||
self.pattern = pattern
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
let m = pattern.matcher(in: element.ownText())
|
||||
return m.find()
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":matchesOwn(\(pattern.toString())"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Exception.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 02/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum ExceptionType {
|
||||
case IllegalArgumentException
|
||||
case IOException
|
||||
case XmlDeclaration
|
||||
case MalformedURLException
|
||||
case CloneNotSupportedException
|
||||
case SelectorParseException
|
||||
}
|
||||
|
||||
public enum Exception: Error {
|
||||
case Error(type:ExceptionType, Message: String)
|
||||
}
|
||||
Generated
+125
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// FormElement.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* A HTML Form Element provides ready access to the form fields/controls that are associated with it. It also allows a
|
||||
* form to easily be submitted.
|
||||
*/
|
||||
public class FormElement: Element {
|
||||
private let _elements: Elements = Elements()
|
||||
|
||||
/**
|
||||
* Create a new, standalone form element.
|
||||
*
|
||||
* @param tag tag of this element
|
||||
* @param baseUri the base URI
|
||||
* @param attributes initial attributes
|
||||
*/
|
||||
public override init(_ tag: Tag, _ baseUri: String, _ attributes: Attributes) {
|
||||
super.init(tag, baseUri, attributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of form control elements associated with this form.
|
||||
* @return form controls associated with this element.
|
||||
*/
|
||||
public func elements() -> Elements {
|
||||
return _elements
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a form control element to this form.
|
||||
* @param element form control to add
|
||||
* @return this form element, for chaining
|
||||
*/
|
||||
@discardableResult
|
||||
public func addElement(_ element: Element) -> FormElement {
|
||||
_elements.add(element)
|
||||
return self
|
||||
}
|
||||
|
||||
//todo:
|
||||
/**
|
||||
* Prepare to submit this form. A Connection object is created with the request set up from the form values. You
|
||||
* can then set up other options (like user-agent, timeout, cookies), then execute it.
|
||||
* @return a connection prepared from the values of this form.
|
||||
* @throws IllegalArgumentException if the form's absolute action URL cannot be determined. Make sure you pass the
|
||||
* document's base URI when parsing.
|
||||
*/
|
||||
// public func submit()throws->Connection {
|
||||
// let action: String = hasAttr("action") ? try absUrl("action") : try baseUri()
|
||||
// Validate.notEmpty(action, "Could not determine a form action URL for submit. Ensure you set a base URI when parsing.")
|
||||
// Connection.Method method = attr("method").toUpperCase().equals("POST") ?
|
||||
// Connection.Method.POST : Connection.Method.GET
|
||||
//
|
||||
// return Jsoup.connect(action)
|
||||
// .data(formData())
|
||||
// .method(method)
|
||||
// }
|
||||
|
||||
//todo:
|
||||
/**
|
||||
* Get the data that this form submits. The returned list is a copy of the data, and changes to the contents of the
|
||||
* list will not be reflected in the DOM.
|
||||
* @return a list of key vals
|
||||
*/
|
||||
// public List<Connection.KeyVal> formData() {
|
||||
// ArrayList<Connection.KeyVal> data = new ArrayList<Connection.KeyVal>();
|
||||
//
|
||||
// // iterate the form control elements and accumulate their values
|
||||
// for (Element el: elements) {
|
||||
// if (!el.tag().isFormSubmittable()) continue; // contents are form listable, superset of submitable
|
||||
// if (el.hasAttr("disabled")) continue; // skip disabled form inputs
|
||||
// String name = el.attr("name");
|
||||
// if (name.length() == 0) continue;
|
||||
// String type = el.attr("type");
|
||||
//
|
||||
// if ("select".equals(el.tagName())) {
|
||||
// Elements options = el.select("option[selected]");
|
||||
// boolean set = false;
|
||||
// for (Element option: options) {
|
||||
// data.add(HttpConnection.KeyVal.create(name, option.val()));
|
||||
// set = true;
|
||||
// }
|
||||
// if (!set) {
|
||||
// Element option = el.select("option").first();
|
||||
// if (option != null)
|
||||
// data.add(HttpConnection.KeyVal.create(name, option.val()));
|
||||
// }
|
||||
// } else if ("checkbox".equalsIgnoreCase(type) || "radio".equalsIgnoreCase(type)) {
|
||||
// // only add checkbox or radio if they have the checked attribute
|
||||
// if (el.hasAttr("checked")) {
|
||||
// final String val = el.val().length() > 0 ? el.val() : "on";
|
||||
// data.add(HttpConnection.KeyVal.create(name, val));
|
||||
// }
|
||||
// } else {
|
||||
// data.add(HttpConnection.KeyVal.create(name, el.val()));
|
||||
// }
|
||||
// }
|
||||
// return data;
|
||||
// }
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let clone = FormElement(_tag, baseUri!, attributes!)
|
||||
return copy(clone: clone)
|
||||
}
|
||||
|
||||
public override func copy(parent: Node?) -> Node {
|
||||
let clone = FormElement(_tag, baseUri!, attributes!)
|
||||
return copy(clone: clone, parent: parent)
|
||||
}
|
||||
public override func copy(clone: Node, parent: Node?) -> Node {
|
||||
let clone = clone as! FormElement
|
||||
for att in _elements.array() {
|
||||
clone._elements.add(att)
|
||||
}
|
||||
return super.copy(clone: clone, parent: parent)
|
||||
}
|
||||
}
|
||||
+771
@@ -0,0 +1,771 @@
|
||||
//
|
||||
// HtmlTreeBuilder.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 24/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* HTML Tree Builder; creates a DOM from Tokens.
|
||||
*/
|
||||
class HtmlTreeBuilder: TreeBuilder {
|
||||
// tag searches
|
||||
public static let TagsSearchInScope: [String] = ["applet", "caption", "html", "table", "td", "th", "marquee", "object"]
|
||||
private static let TagSearchList: [String] = ["ol", "ul"]
|
||||
private static let TagSearchButton: [String] = ["button"]
|
||||
private static let TagSearchTableScope: [String] = ["html", "table"]
|
||||
private static let TagSearchSelectScope: [String] = ["optgroup", "option"]
|
||||
private static let TagSearchEndTags: [String] = ["dd", "dt", "li", "option", "optgroup", "p", "rp", "rt"]
|
||||
private static let TagSearchSpecial: [String] = ["address", "applet", "area", "article", "aside", "base", "basefont", "bgsound",
|
||||
"blockquote", "body", "br", "button", "caption", "center", "col", "colgroup", "command", "dd",
|
||||
"details", "dir", "div", "dl", "dt", "embed", "fieldset", "figcaption", "figure", "footer", "form",
|
||||
"frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html",
|
||||
"iframe", "img", "input", "isindex", "li", "link", "listing", "marquee", "menu", "meta", "nav",
|
||||
"noembed", "noframes", "noscript", "object", "ol", "p", "param", "plaintext", "pre", "script",
|
||||
"section", "select", "style", "summary", "table", "tbody", "td", "textarea", "tfoot", "th", "thead",
|
||||
"title", "tr", "ul", "wbr", "xmp"]
|
||||
|
||||
private var _state: HtmlTreeBuilderState = HtmlTreeBuilderState.Initial // the current state
|
||||
private var _originalState: HtmlTreeBuilderState = HtmlTreeBuilderState.Initial // original / marked state
|
||||
|
||||
private var baseUriSetFromDoc: Bool = false
|
||||
private var headElement: Element? // the current head element
|
||||
private var formElement: FormElement? // the current form element
|
||||
private var contextElement: Element? // fragment parse context -- could be null even if fragment parsing
|
||||
private var formattingElements: Array<Element?> = Array<Element?>() // active (open) formatting elements
|
||||
private var pendingTableCharacters: Array<String> = Array<String>() // chars in table to be shifted out
|
||||
private var emptyEnd: Token.EndTag = Token.EndTag() // reused empty end tag
|
||||
|
||||
private var _framesetOk: Bool = true // if ok to go into frameset
|
||||
private var fosterInserts: Bool = false // if next inserts should be fostered
|
||||
private var fragmentParsing: Bool = false // if parsing a fragment of html
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public override func defaultSettings() -> ParseSettings {
|
||||
return ParseSettings.htmlDefault
|
||||
}
|
||||
|
||||
override func parse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings)throws->Document {
|
||||
_state = HtmlTreeBuilderState.Initial
|
||||
baseUriSetFromDoc = false
|
||||
return try super.parse(input, baseUri, errors, settings)
|
||||
}
|
||||
|
||||
func parseFragment(_ inputFragment: String, _ context: Element?, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings)throws->Array<Node> {
|
||||
// context may be null
|
||||
_state = HtmlTreeBuilderState.Initial
|
||||
initialiseParse(inputFragment, baseUri, errors, settings)
|
||||
contextElement = context
|
||||
fragmentParsing = true
|
||||
var root: Element? = nil
|
||||
|
||||
if let context = context {
|
||||
if let d = context.ownerDocument() { // quirks setup:
|
||||
doc.quirksMode(d.quirksMode())
|
||||
}
|
||||
|
||||
// initialise the tokeniser state:
|
||||
let contextTag: String = context.tagName()
|
||||
if (StringUtil.inString(contextTag, haystack: "title", "textarea")) {
|
||||
tokeniser.transition(TokeniserState.Rcdata)
|
||||
} else if (StringUtil.inString(contextTag, haystack: "iframe", "noembed", "noframes", "style", "xmp")) {
|
||||
tokeniser.transition(TokeniserState.Rawtext)
|
||||
} else if (contextTag=="script") {
|
||||
tokeniser.transition(TokeniserState.ScriptData)
|
||||
} else if (contextTag==("noscript")) {
|
||||
tokeniser.transition(TokeniserState.Data) // if scripting enabled, rawtext
|
||||
} else if (contextTag=="plaintext") {
|
||||
tokeniser.transition(TokeniserState.Data)
|
||||
} else {
|
||||
tokeniser.transition(TokeniserState.Data) // default
|
||||
}
|
||||
|
||||
root = try Element(Tag.valueOf("html", settings), baseUri)
|
||||
try Validate.notNull(obj: root)
|
||||
try doc.appendChild(root!)
|
||||
stack.append(root!)
|
||||
resetInsertionMode()
|
||||
|
||||
// setup form element to nearest form on context (up ancestor chain). ensures form controls are associated
|
||||
// with form correctly
|
||||
let contextChain: Elements = context.parents()
|
||||
contextChain.add(0, context)
|
||||
for parent: Element in contextChain.array() {
|
||||
if let x = (parent as? FormElement) {
|
||||
formElement = x
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try runParser()
|
||||
if (context != nil && root != nil) {
|
||||
return root!.getChildNodes()
|
||||
} else {
|
||||
return doc.getChildNodes()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public override func process(_ token: Token)throws->Bool {
|
||||
currentToken = token
|
||||
return try self._state.process(token, self)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func process(_ token: Token, _ state: HtmlTreeBuilderState)throws->Bool {
|
||||
currentToken = token
|
||||
return try state.process(token, self)
|
||||
}
|
||||
|
||||
func transition(_ state: HtmlTreeBuilderState) {
|
||||
self._state = state
|
||||
}
|
||||
|
||||
func state() -> HtmlTreeBuilderState {
|
||||
return _state
|
||||
}
|
||||
|
||||
func markInsertionMode() {
|
||||
_originalState = _state
|
||||
}
|
||||
|
||||
func originalState() -> HtmlTreeBuilderState {
|
||||
return _originalState
|
||||
}
|
||||
|
||||
func framesetOk(_ framesetOk: Bool) {
|
||||
self._framesetOk = framesetOk
|
||||
}
|
||||
|
||||
func framesetOk() -> Bool {
|
||||
return _framesetOk
|
||||
}
|
||||
|
||||
func getDocument() -> Document {
|
||||
return doc
|
||||
}
|
||||
|
||||
func getBaseUri() -> String {
|
||||
return baseUri
|
||||
}
|
||||
|
||||
func maybeSetBaseUri(_ base: Element)throws {
|
||||
if (baseUriSetFromDoc) { // only listen to the first <base href> in parse
|
||||
return
|
||||
}
|
||||
|
||||
let href: String = try base.absUrl("href")
|
||||
if (href.count != 0) { // ignore <base target> etc
|
||||
baseUri = href
|
||||
baseUriSetFromDoc = true
|
||||
try doc.setBaseUri(href) // set on the doc so doc.createElement(Tag) will get updated base, and to update all descendants
|
||||
}
|
||||
}
|
||||
|
||||
func isFragmentParsing() -> Bool {
|
||||
return fragmentParsing
|
||||
}
|
||||
|
||||
func error(_ state: HtmlTreeBuilderState) {
|
||||
if (errors.canAddError() && currentToken != nil) {
|
||||
errors.add(ParseError(reader.getPos(), "Unexpected token [\(currentToken!.tokenType())] when in state [\(state.rawValue)]"))
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert(_ startTag: Token.StartTag)throws->Element {
|
||||
// handle empty unknown tags
|
||||
// when the spec expects an empty tag, will directly hit insertEmpty, so won't generate this fake end tag.
|
||||
if (startTag.isSelfClosing()) {
|
||||
let el: Element = try insertEmpty(startTag)
|
||||
stack.append(el)
|
||||
tokeniser.transition(TokeniserState.Data) // handles <script />, otherwise needs breakout steps from script data
|
||||
try tokeniser.emit(emptyEnd.reset().name(el.tagName())) // ensure we get out of whatever state we are in. emitted for yielded processing
|
||||
return el
|
||||
}
|
||||
try Validate.notNull(obj: startTag._attributes)
|
||||
let el: Element = try Element(Tag.valueOf(startTag.name(), settings), baseUri, settings.normalizeAttributes(startTag._attributes))
|
||||
try insert(el)
|
||||
return el
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insertStartTag(_ startTagName: String)throws->Element {
|
||||
let el: Element = try Element(Tag.valueOf(startTagName, settings), baseUri)
|
||||
try insert(el)
|
||||
return el
|
||||
}
|
||||
|
||||
func insert(_ el: Element)throws {
|
||||
try insertNode(el)
|
||||
stack.append(el)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insertEmpty(_ startTag: Token.StartTag)throws->Element {
|
||||
let tag: Tag = try Tag.valueOf(startTag.name(), settings)
|
||||
try Validate.notNull(obj: startTag._attributes)
|
||||
let el: Element = Element(tag, baseUri, startTag._attributes)
|
||||
try insertNode(el)
|
||||
if (startTag.isSelfClosing()) {
|
||||
if (tag.isKnownTag()) {
|
||||
if (tag.isSelfClosing()) {tokeniser.acknowledgeSelfClosingFlag()} // if not acked, promulagates error
|
||||
} else {
|
||||
// unknown tag, remember this is self closing for output
|
||||
tag.setSelfClosing()
|
||||
tokeniser.acknowledgeSelfClosingFlag() // not an distinct error
|
||||
}
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insertForm(_ startTag: Token.StartTag, _ onStack: Bool)throws->FormElement {
|
||||
let tag: Tag = try Tag.valueOf(startTag.name(), settings)
|
||||
try Validate.notNull(obj: startTag._attributes)
|
||||
let el: FormElement = FormElement(tag, baseUri, startTag._attributes)
|
||||
setFormElement(el)
|
||||
try insertNode(el)
|
||||
if (onStack) {
|
||||
stack.append(el)
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
func insert(_ commentToken: Token.Comment)throws {
|
||||
let comment: Comment = Comment(commentToken.getData(), baseUri)
|
||||
try insertNode(comment)
|
||||
}
|
||||
|
||||
func insert(_ characterToken: Token.Char)throws {
|
||||
var node: Node
|
||||
// characters in script and style go in as datanodes, not text nodes
|
||||
let tagName: String? = currentElement()?.tagName()
|
||||
if (tagName=="script" || tagName=="style") {
|
||||
try Validate.notNull(obj: characterToken.getData())
|
||||
node = DataNode(characterToken.getData()!, baseUri)
|
||||
} else {
|
||||
try Validate.notNull(obj: characterToken.getData())
|
||||
node = TextNode(characterToken.getData()!, baseUri)
|
||||
}
|
||||
try currentElement()?.appendChild(node) // doesn't use insertNode, because we don't foster these; and will always have a stack.
|
||||
}
|
||||
|
||||
private func insertNode(_ node: Node)throws {
|
||||
// if the stack hasn't been set up yet, elements (doctype, comments) go into the doc
|
||||
if (stack.count == 0) {
|
||||
try doc.appendChild(node)
|
||||
} else if (isFosterInserts()) {
|
||||
try insertInFosterParent(node)
|
||||
} else {
|
||||
try currentElement()?.appendChild(node)
|
||||
}
|
||||
|
||||
// connect form controls to their form element
|
||||
if let n = (node as? Element) {
|
||||
if(n.tag().isFormListed()) {
|
||||
if ( formElement != nil) {
|
||||
formElement!.addElement(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pop() -> Element {
|
||||
let size: Int = stack.count
|
||||
return stack.remove(at: size-1)
|
||||
}
|
||||
|
||||
func push(_ element: Element) {
|
||||
stack.append(element)
|
||||
}
|
||||
|
||||
func getStack()->Array<Element> {
|
||||
return stack
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func onStack(_ el: Element) -> Bool {
|
||||
return isElementInQueue(stack, el)
|
||||
}
|
||||
|
||||
private func isElementInQueue(_ queue: Array<Element?>, _ element: Element?) -> Bool {
|
||||
for pos in (0..<queue.count).reversed() {
|
||||
let next: Element? = queue[pos]
|
||||
if (next == element) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getFromStack(_ elName: String) -> Element? {
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
if next.nodeName() == elName {
|
||||
return next
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func removeFromStack(_ el: Element) -> Bool {
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
if (next == el) {
|
||||
stack.remove(at: pos)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func popStackToClose(_ elName: String) {
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
stack.remove(at: pos)
|
||||
if (next.nodeName() == elName) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func popStackToClose(_ elNames: String...) {
|
||||
popStackToClose(elNames)
|
||||
}
|
||||
func popStackToClose(_ elNames: [String]) {
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
stack.remove(at: pos)
|
||||
if (StringUtil.inString(next.nodeName(), elNames)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func popStackToBefore(_ elName: String) {
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
if (next.nodeName() == elName) {
|
||||
break
|
||||
} else {
|
||||
stack.remove(at: pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clearStackToTableContext() {
|
||||
clearStackToContext("table")
|
||||
}
|
||||
|
||||
func clearStackToTableBodyContext() {
|
||||
clearStackToContext("tbody", "tfoot", "thead")
|
||||
}
|
||||
|
||||
func clearStackToTableRowContext() {
|
||||
clearStackToContext("tr")
|
||||
}
|
||||
|
||||
private func clearStackToContext(_ nodeNames: String...) {
|
||||
clearStackToContext(nodeNames)
|
||||
}
|
||||
private func clearStackToContext(_ nodeNames: [String]) {
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
if (StringUtil.inString(next.nodeName(), nodeNames) || next.nodeName()=="html") {
|
||||
break
|
||||
} else {
|
||||
stack.remove(at: pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func aboveOnStack(_ el: Element) -> Element? {
|
||||
//assert(onStack(el), "Invalid parameter")
|
||||
onStack(el)
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
if (next == el) {
|
||||
return stack[pos-1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertOnStackAfter(_ after: Element, _ input: Element)throws {
|
||||
let i: Int = stack.lastIndexOf(after)
|
||||
try Validate.isTrue(val: i != -1)
|
||||
stack.insert(input, at: i + 1 )
|
||||
}
|
||||
|
||||
func replaceOnStack(_ out: Element, _ input: Element)throws {
|
||||
try stack = replaceInQueue(stack, out, input)
|
||||
}
|
||||
|
||||
private func replaceInQueue(_ queue: Array<Element>, _ out: Element, _ input: Element)throws->Array<Element> {
|
||||
var queue = queue
|
||||
let i: Int = queue.lastIndexOf(out)
|
||||
try Validate.isTrue(val: i != -1)
|
||||
queue[i] = input
|
||||
return queue
|
||||
}
|
||||
|
||||
private func replaceInQueue(_ queue: Array<Element?>, _ out: Element, _ input: Element)throws->Array<Element?> {
|
||||
var queue = queue
|
||||
var i: Int = -1
|
||||
for index in 0..<queue.count {
|
||||
if(out == queue[index]) {
|
||||
i = index
|
||||
}
|
||||
}
|
||||
try Validate.isTrue(val: i != -1)
|
||||
queue[i] = input
|
||||
return queue
|
||||
}
|
||||
|
||||
func resetInsertionMode() {
|
||||
var last = false
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
var node: Element = stack[pos]
|
||||
if (pos == 0) {
|
||||
last = true
|
||||
//Validate node
|
||||
node = contextElement!
|
||||
}
|
||||
let name: String = node.nodeName()
|
||||
if ("select".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InSelect)
|
||||
break // frag
|
||||
} else if (("td".equals(name) || "th".equals(name) && !last)) {
|
||||
transition(HtmlTreeBuilderState.InCell)
|
||||
break
|
||||
} else if ("tr".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InRow)
|
||||
break
|
||||
} else if ("tbody".equals(name) || "thead".equals(name) || "tfoot".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InTableBody)
|
||||
break
|
||||
} else if ("caption".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InCaption)
|
||||
break
|
||||
} else if ("colgroup".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InColumnGroup)
|
||||
break // frag
|
||||
} else if ("table".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InTable)
|
||||
break
|
||||
} else if ("head".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InBody)
|
||||
break // frag
|
||||
} else if ("body".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InBody)
|
||||
break
|
||||
} else if ("frameset".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.InFrameset)
|
||||
break // frag
|
||||
} else if ("html".equals(name)) {
|
||||
transition(HtmlTreeBuilderState.BeforeHead)
|
||||
break // frag
|
||||
} else if (last) {
|
||||
transition(HtmlTreeBuilderState.InBody)
|
||||
break // frag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: tidy up in specific scope methods
|
||||
private var specificScopeTarget: [String?] = [nil]
|
||||
|
||||
private func inSpecificScope(_ targetName: String, _ baseTypes: [String], _ extraTypes: [String]?)throws->Bool {
|
||||
specificScopeTarget[0] = targetName
|
||||
return try inSpecificScope(specificScopeTarget, baseTypes, extraTypes)
|
||||
}
|
||||
|
||||
private func inSpecificScope(_ targetNames: [String?], _ baseTypes: [String], _ extraTypes: [String]?)throws->Bool {
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let el: Element = stack[pos]
|
||||
let elName: String = el.nodeName()
|
||||
if (StringUtil.inString(elName, targetNames)) {
|
||||
return true
|
||||
}
|
||||
if (StringUtil.inString(elName, baseTypes)) {
|
||||
return false
|
||||
}
|
||||
if (extraTypes != nil && StringUtil.inString(elName, extraTypes!)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try Validate.fail(msg: "Should not be reachable")
|
||||
return false
|
||||
}
|
||||
|
||||
func inScope(_ targetNames: [String])throws->Bool {
|
||||
return try inSpecificScope(targetNames, HtmlTreeBuilder.TagsSearchInScope, nil)
|
||||
}
|
||||
|
||||
func inScope(_ targetName: String)throws->Bool {
|
||||
return try inScope(targetName, nil)
|
||||
}
|
||||
|
||||
func inScope(_ targetName: String, _ extras: [String]?)throws->Bool {
|
||||
return try inSpecificScope(targetName, HtmlTreeBuilder.TagsSearchInScope, extras)
|
||||
// todo: in mathml namespace: mi, mo, mn, ms, mtext annotation-xml
|
||||
// todo: in svg namespace: forignOjbect, desc, title
|
||||
}
|
||||
|
||||
func inListItemScope(_ targetName: String)throws->Bool {
|
||||
return try inScope(targetName, HtmlTreeBuilder.TagSearchList)
|
||||
}
|
||||
|
||||
func inButtonScope(_ targetName: String)throws->Bool {
|
||||
return try inScope(targetName, HtmlTreeBuilder.TagSearchButton)
|
||||
}
|
||||
|
||||
func inTableScope(_ targetName: String)throws->Bool {
|
||||
return try inSpecificScope(targetName, HtmlTreeBuilder.TagSearchTableScope, nil)
|
||||
}
|
||||
|
||||
func inSelectScope(_ targetName: String)throws->Bool {
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let el: Element = stack[pos]
|
||||
let elName: String = el.nodeName()
|
||||
if (elName.equals(targetName)) {
|
||||
return true
|
||||
}
|
||||
if (!StringUtil.inString(elName, HtmlTreeBuilder.TagSearchSelectScope)) { // all elements except
|
||||
return false
|
||||
}
|
||||
}
|
||||
try Validate.fail(msg: "Should not be reachable")
|
||||
return false
|
||||
}
|
||||
|
||||
func setHeadElement(_ headElement: Element) {
|
||||
self.headElement = headElement
|
||||
}
|
||||
|
||||
func getHeadElement() -> Element? {
|
||||
return headElement
|
||||
}
|
||||
|
||||
func isFosterInserts() -> Bool {
|
||||
return fosterInserts
|
||||
}
|
||||
|
||||
func setFosterInserts(_ fosterInserts: Bool) {
|
||||
self.fosterInserts = fosterInserts
|
||||
}
|
||||
|
||||
func getFormElement() -> FormElement? {
|
||||
return formElement
|
||||
}
|
||||
|
||||
func setFormElement(_ formElement: FormElement?) {
|
||||
self.formElement = formElement
|
||||
}
|
||||
|
||||
func newPendingTableCharacters() {
|
||||
pendingTableCharacters = Array<String>()
|
||||
}
|
||||
|
||||
func getPendingTableCharacters()->Array<String> {
|
||||
return pendingTableCharacters
|
||||
}
|
||||
|
||||
func setPendingTableCharacters(_ pendingTableCharacters: Array<String>) {
|
||||
self.pendingTableCharacters = pendingTableCharacters
|
||||
}
|
||||
|
||||
/**
|
||||
11.2.5.2 Closing elements that have implied end tags<p/>
|
||||
When the steps below require the UA to generate implied end tags, then, while the current node is a dd element, a
|
||||
dt element, an li element, an option element, an optgroup element, a p element, an rp element, or an rt element,
|
||||
the UA must pop the current node off the stack of open elements.
|
||||
|
||||
@param excludeTag If a step requires the UA to generate implied end tags but lists an element to exclude from the
|
||||
process, then the UA must perform the above steps as if that element was not in the above list.
|
||||
*/
|
||||
|
||||
func generateImpliedEndTags(_ excludeTag: String?) {
|
||||
|
||||
while ((excludeTag != nil && !currentElement()!.nodeName().equals(excludeTag!)) &&
|
||||
StringUtil.inString(currentElement()!.nodeName(), HtmlTreeBuilder.TagSearchEndTags)) {
|
||||
pop()
|
||||
}
|
||||
}
|
||||
|
||||
func generateImpliedEndTags() {
|
||||
generateImpliedEndTags(nil)
|
||||
}
|
||||
|
||||
func isSpecial(_ el: Element) -> Bool {
|
||||
// todo: mathml's mi, mo, mn
|
||||
// todo: svg's foreigObject, desc, title
|
||||
let name: String = el.nodeName()
|
||||
return StringUtil.inString(name, HtmlTreeBuilder.TagSearchSpecial)
|
||||
}
|
||||
|
||||
func lastFormattingElement() -> Element? {
|
||||
return formattingElements.count > 0 ? formattingElements[formattingElements.count-1] : nil
|
||||
}
|
||||
|
||||
func removeLastFormattingElement() -> Element? {
|
||||
let size: Int = formattingElements.count
|
||||
if (size > 0) {
|
||||
return formattingElements.remove(at: size-1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// active formatting elements
|
||||
func pushActiveFormattingElements(_ input: Element) {
|
||||
var numSeen: Int = 0
|
||||
for pos in (0..<formattingElements.count).reversed() {
|
||||
let el: Element? = formattingElements[pos]
|
||||
if (el == nil) { // marker
|
||||
break
|
||||
}
|
||||
|
||||
if (isSameFormattingElement(input, el!)) {
|
||||
numSeen += 1
|
||||
}
|
||||
|
||||
if (numSeen == 3) {
|
||||
formattingElements.remove(at: pos)
|
||||
break
|
||||
}
|
||||
}
|
||||
formattingElements.append(input)
|
||||
}
|
||||
|
||||
private func isSameFormattingElement(_ a: Element, _ b: Element) -> Bool {
|
||||
// same if: same namespace, tag, and attributes. Element.equals only checks tag, might in future check children
|
||||
if(a.attributes == nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
return a.nodeName().equals(b.nodeName()) &&
|
||||
// a.namespace().equals(b.namespace()) &&
|
||||
a.getAttributes()!.equals(o: b.getAttributes())
|
||||
// todo: namespaces
|
||||
}
|
||||
|
||||
func reconstructFormattingElements()throws {
|
||||
let last: Element? = lastFormattingElement()
|
||||
if (last == nil || onStack(last!)) {
|
||||
return
|
||||
}
|
||||
|
||||
var entry: Element? = last
|
||||
let size: Int = formattingElements.count
|
||||
var pos: Int = size - 1
|
||||
var skip: Bool = false
|
||||
while (true) {
|
||||
if (pos == 0) { // step 4. if none before, skip to 8
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
pos -= 1
|
||||
entry = formattingElements[pos] // step 5. one earlier than entry
|
||||
if (entry == nil || onStack(entry!)) // step 6 - neither marker nor on stack
|
||||
{break} // jump to 8, else continue back to 4
|
||||
}
|
||||
while(true) {
|
||||
if (!skip) // step 7: on later than entry
|
||||
{
|
||||
pos += 1
|
||||
entry = formattingElements[pos]
|
||||
}
|
||||
try Validate.notNull(obj: entry) // should not occur, as we break at last element
|
||||
|
||||
// 8. create new element from element, 9 insert into current node, onto stack
|
||||
skip = false // can only skip increment from 4.
|
||||
let newEl: Element = try insertStartTag(entry!.nodeName()) // todo: avoid fostering here?
|
||||
// newEl.namespace(entry.namespace()) // todo: namespaces
|
||||
newEl.getAttributes()?.addAll(incoming: entry!.getAttributes())
|
||||
|
||||
// 10. replace entry with new entry
|
||||
formattingElements[pos] = newEl
|
||||
|
||||
// 11
|
||||
if (pos == size-1) // if not last entry in list, jump to 7
|
||||
{break}
|
||||
}
|
||||
}
|
||||
|
||||
func clearFormattingElementsToLastMarker() {
|
||||
while (!formattingElements.isEmpty) {
|
||||
let el: Element? = removeLastFormattingElement()
|
||||
if (el == nil) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeFromActiveFormattingElements(_ el: Element?) {
|
||||
for pos in (0..<formattingElements.count).reversed() {
|
||||
let next: Element? = formattingElements[pos]
|
||||
if (next == el) {
|
||||
formattingElements.remove(at: pos)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isInActiveFormattingElements(_ el: Element) -> Bool {
|
||||
return isElementInQueue(formattingElements, el)
|
||||
}
|
||||
|
||||
func getActiveFormattingElement(_ nodeName: String) -> Element? {
|
||||
for pos in (0..<formattingElements.count).reversed() {
|
||||
let next: Element? = formattingElements[pos]
|
||||
if (next == nil) { // scope marker
|
||||
break
|
||||
} else if (next!.nodeName().equals(nodeName)) {
|
||||
return next
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceActiveFormattingElement(_ out: Element, _ input: Element)throws {
|
||||
try formattingElements = replaceInQueue(formattingElements, out, input)
|
||||
}
|
||||
|
||||
func insertMarkerToFormattingElements() {
|
||||
formattingElements.append(nil)
|
||||
}
|
||||
|
||||
func insertInFosterParent(_ input: Node)throws {
|
||||
let fosterParent: Element?
|
||||
let lastTable: Element? = getFromStack("table")
|
||||
var isLastTableParent: Bool = false
|
||||
if let lastTable = lastTable {
|
||||
if (lastTable.parent() != nil) {
|
||||
fosterParent = lastTable.parent()!
|
||||
isLastTableParent = true
|
||||
} else {
|
||||
fosterParent = aboveOnStack(lastTable)
|
||||
}
|
||||
} else { // no table == frag
|
||||
fosterParent = stack[0]
|
||||
}
|
||||
|
||||
if (isLastTableParent) {
|
||||
try Validate.notNull(obj: lastTable) // last table cannot be null by this point.
|
||||
try lastTable!.before(input)
|
||||
} else {
|
||||
try fosterParent?.appendChild(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1549
File diff suppressed because it is too large
Load Diff
+10
@@ -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:
|
||||
Generated
+802
@@ -0,0 +1,802 @@
|
||||
//
|
||||
// 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<Node> = Array<Node>()
|
||||
weak var parentNode: Node?
|
||||
var childNodes: Array <Node>
|
||||
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. <b>Case insensitive</b>
|
||||
* <p>
|
||||
* To get an absolute URL from an attribute that may be a relative URL, prefix the key with <code><b>abs</b></code>,
|
||||
* which is a shortcut to the {@link #absUrl} method.
|
||||
* </p>
|
||||
* E.g.:
|
||||
* <blockquote><code>String url = a.attr("abs:href");</code></blockquote>
|
||||
*
|
||||
* @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. <b>Case insensitive</b>
|
||||
* @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 <code><a href></code> or
|
||||
* <code><img src></code>).
|
||||
* <p>
|
||||
* E.g.: <code>String absUrl = linkEl.absUrl("href");</code>
|
||||
* </p>
|
||||
* <p>
|
||||
* If the attribute value is already absolute (i.e. it starts with a protocol, like
|
||||
* <code>http://</code> or <code>https://</code> 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* As an alternate, you can use the {@link #attr} method with the <code>abs:</code> prefix, e.g.:
|
||||
* <code>String absUrl = linkEl.attr("abs:href");</code>
|
||||
* </p>
|
||||
*
|
||||
* @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<Node> {
|
||||
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<Node> {
|
||||
var children: Array<Node> = Array<Node>()
|
||||
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<Node> = 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<Node> = 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 <div class="head"></div>}. 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<Node> = 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 <div></div><p></p> -- The <p> is remainder
|
||||
if (wrapChildren.count > 0) {
|
||||
for i in 0..<wrapChildren.count {
|
||||
let remainder: Node = wrapChildren[i]
|
||||
try remainder.parentNode?.removeChild(remainder)
|
||||
try wrap.appendChild(remainder)
|
||||
}
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this node from the DOM, and moves its children up into the node's parent. This has the effect of dropping
|
||||
* the node but keeping its children.
|
||||
* <p>
|
||||
* For example, with the input html:
|
||||
* </p>
|
||||
* <p>{@code <div>One <span>Two <b>Three</b></span></div>}</p>
|
||||
* Calling {@code element.unwrap()} on the {@code span} element will result in the html:
|
||||
* <p>{@code <div>One Two <b>Three</b></div>}</p>
|
||||
* 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..<children.count).reversed() {
|
||||
let input: Node = children[i]
|
||||
try reparentChild(input)
|
||||
childNodes.insert(input, at: index)
|
||||
reindexChildren(index)
|
||||
}
|
||||
}
|
||||
|
||||
public func ensureChildNodes() {
|
||||
// if (childNodes === Node.EMPTY_NODES) {
|
||||
// childNodes = Array<Node>()
|
||||
// }
|
||||
}
|
||||
|
||||
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..<childNodes.count {
|
||||
childNodes[i].setSiblingIndex(i)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves this node's sibling nodes. Similar to {@link #childNodes() node.parent.childNodes()}, but does not
|
||||
include this node (a node is not a sibling of itself).
|
||||
@return node siblings. If the node has no parent, returns an empty list.
|
||||
*/
|
||||
open func siblingNodes()->Array<Node> {
|
||||
if (parentNode == nil) {
|
||||
return Array<Node>()
|
||||
}
|
||||
|
||||
let nodes: Array<Node> = parentNode!.childNodes
|
||||
var siblings: Array<Node> = Array<Node>()
|
||||
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<Node> = 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.
|
||||
* <p>
|
||||
* 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<Node> = Array<Node>()
|
||||
nodesToProcess.append(thisClone)
|
||||
|
||||
while (!nodesToProcess.isEmpty) {
|
||||
let currParent: Node = nodesToProcess.removeFirst()
|
||||
|
||||
for i in 0..<currParent.childNodes.count {
|
||||
let childClone: Node = currParent.childNodes[i].copy(parent: currParent)
|
||||
currParent.childNodes[i] = childClone
|
||||
nodesToProcess.append(childClone)
|
||||
}
|
||||
}
|
||||
return thisClone
|
||||
}
|
||||
|
||||
/*
|
||||
* Return a clone of the node using the given parent (which can be null).
|
||||
* Not a deep copy of children.
|
||||
*/
|
||||
public func copy(clone: Node, parent: Node?) -> 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<Node>()
|
||||
|
||||
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 {
|
||||
if (!(node.nodeName() == OuterHtmlVisitor.text)) { // saves a void hit.
|
||||
try node.outerHtmlTail(accum, depth, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 var hashValue: Int {
|
||||
return description.hashValue ^ (baseUri?.hashValue ?? 31)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// NodeTraversor.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 17/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class NodeTraversor {
|
||||
private let visitor: NodeVisitor
|
||||
|
||||
/**
|
||||
* Create a new traversor.
|
||||
* @param visitor a class implementing the {@link NodeVisitor} interface, to be called when visiting each node.
|
||||
*/
|
||||
public init(_ visitor: NodeVisitor) {
|
||||
self.visitor = visitor
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a depth-first traverse of the root and all of its descendants.
|
||||
* @param root the root node point to traverse.
|
||||
*/
|
||||
open func traverse(_ root: Node?)throws {
|
||||
var node: Node? = root
|
||||
var depth: Int = 0
|
||||
|
||||
while (node != nil) {
|
||||
try visitor.head(node!, depth)
|
||||
if (node!.childNodeSize() > 0) {
|
||||
node = node!.childNode(0)
|
||||
depth+=1
|
||||
} else {
|
||||
while (node!.nextSibling() == nil && depth > 0) {
|
||||
try visitor.tail(node!, depth)
|
||||
node = node!.getParentNode()
|
||||
depth-=1
|
||||
}
|
||||
try visitor.tail(node!, depth)
|
||||
if (node === root) {
|
||||
break
|
||||
}
|
||||
node = node!.nextSibling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+37
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// NodeVisitor.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 16/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Node visitor interface. Provide an implementing class to {@link NodeTraversor} to iterate through nodes.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*/
|
||||
public protocol NodeVisitor {
|
||||
/**
|
||||
* Callback for when a node is first visited.
|
||||
*
|
||||
* @param node the node being visited.
|
||||
* @param depth the depth of the node, relative to the root node. E.g., the root node has depth 0, and a child node
|
||||
* of that will have depth 1.
|
||||
*/
|
||||
func head(_ node: Node, _ depth: Int)throws
|
||||
|
||||
/**
|
||||
* Callback for when a node is last visited, after all of its descendants have been visited.
|
||||
*
|
||||
* @param node the node being visited.
|
||||
* @param depth the depth of the node, relative to the root node. E.g., the root node has depth 0, and a child node
|
||||
* of that will have depth 1.
|
||||
*/
|
||||
func tail(_ node: Node, _ depth: Int)throws
|
||||
}
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
//
|
||||
// LinkedHashMap.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 03/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class OrderedDictionary<Key: Hashable, Value: Equatable>: MutableCollection, Hashable {
|
||||
|
||||
/// Returns the position immediately after the given index.
|
||||
///
|
||||
/// - Parameter i: A valid index of the collection. `i` must be less than
|
||||
/// `endIndex`.
|
||||
/// - Returns: The index value immediately after `i`.
|
||||
public func index(after i: Int) -> Int {
|
||||
return _orderedKeys.index(after: i)
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Type Aliases
|
||||
// ======================================================= //
|
||||
|
||||
public typealias Element = (Key, Value)
|
||||
|
||||
public typealias Index = Int
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Initialization
|
||||
// ======================================================= //
|
||||
|
||||
public init() {}
|
||||
public init(count: Int) {}
|
||||
|
||||
public init(elements: [Element]) {
|
||||
for (key, value) in elements {
|
||||
self[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
public func copy() -> Any {
|
||||
return copy(with: nil)
|
||||
}
|
||||
|
||||
public func mutableCopy(with zone: NSZone? = nil) -> Any {
|
||||
return copy()
|
||||
}
|
||||
|
||||
public func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = OrderedDictionary<Key, Value>()
|
||||
//let copy = type(of:self).init()
|
||||
for element in orderedKeys {
|
||||
copy.put(value: valueForKey(key: element)!, forKey: element)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func clone() -> OrderedDictionary<Key, Value> {
|
||||
return copy() as! OrderedDictionary<Key, Value>
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Accessing Keys & Values
|
||||
// ======================================================= //
|
||||
|
||||
public var orderedKeys: [Key] {
|
||||
return _orderedKeys
|
||||
}
|
||||
|
||||
public func keySet() -> [Key] {
|
||||
return _orderedKeys
|
||||
}
|
||||
|
||||
public var orderedValues: [Value] {
|
||||
#if !swift(>=4.1)
|
||||
return _orderedKeys.flatMap { _keysToValues[$0] }
|
||||
#else
|
||||
return _orderedKeys.compactMap { _keysToValues[$0] }
|
||||
#endif
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Managing Content Using Keys
|
||||
// ======================================================= //
|
||||
|
||||
public subscript(key: Key) -> Value? {
|
||||
get {
|
||||
return valueForKey(key: key)
|
||||
}
|
||||
set(newValue) {
|
||||
if let newValue = newValue {
|
||||
updateValue(value: newValue, forKey: key)
|
||||
} else {
|
||||
removeValueForKey(key: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func containsKey(key: Key) -> Bool {
|
||||
return _orderedKeys.contains(key)
|
||||
}
|
||||
|
||||
public func valueForKey(key: Key) -> Value? {
|
||||
return _keysToValues[key]
|
||||
}
|
||||
|
||||
public func get(key: Key) -> Value? {
|
||||
return valueForKey(key: key)
|
||||
}
|
||||
|
||||
// required var for the Hashable protocol
|
||||
public var hashValue: Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
public func hashCode() -> Int {
|
||||
return hashValue
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func updateValue(value: Value, forKey key: Key) -> Value? {
|
||||
|
||||
guard let currentValue = _keysToValues[key] else {
|
||||
_orderedKeys.append(key)
|
||||
_keysToValues[key] = value
|
||||
return nil
|
||||
}
|
||||
_keysToValues[key] = value
|
||||
return currentValue
|
||||
|
||||
// if _orderedKeys.contains(key) {
|
||||
// guard let currentValue = _keysToValues[key] else {
|
||||
// fatalError("Inconsistency error occured in OrderedDictionary")
|
||||
// }
|
||||
//
|
||||
// _keysToValues[key] = value
|
||||
//
|
||||
// return currentValue
|
||||
// } else {
|
||||
// _orderedKeys.append(key)
|
||||
// _keysToValues[key] = value
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
}
|
||||
|
||||
public func put(value: Value, forKey key: Key) {
|
||||
self[key] = value
|
||||
}
|
||||
|
||||
public func putAll(all: OrderedDictionary<Key, Value>) {
|
||||
for i in all.orderedKeys {
|
||||
put(value: all[i]!, forKey: i)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func removeValueForKey(key: Key) -> Value? {
|
||||
if let index = _orderedKeys.index(of: key) {
|
||||
guard let currentValue = _keysToValues[key] else {
|
||||
fatalError("Inconsistency error occured in OrderedDictionary")
|
||||
}
|
||||
|
||||
_orderedKeys.remove(at: index)
|
||||
_keysToValues[key] = nil
|
||||
|
||||
return currentValue
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func remove(key: Key) -> Value? {
|
||||
return removeValueForKey(key: key)
|
||||
}
|
||||
|
||||
public func removeAll(keepCapacity: Bool = true) {
|
||||
_orderedKeys.removeAll(keepingCapacity: keepCapacity)
|
||||
_keysToValues.removeAll(keepingCapacity: keepCapacity)
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Managing Content Using Indexes
|
||||
// ======================================================= //
|
||||
|
||||
public subscript(index: Index) -> Element {
|
||||
get {
|
||||
guard let element = elementAtIndex(index: index) else {
|
||||
fatalError("OrderedDictionary index out of range")
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
set(newValue) {
|
||||
updateElement(element: newValue, atIndex: index)
|
||||
}
|
||||
}
|
||||
|
||||
public func indexForKey(key: Key) -> Index? {
|
||||
return _orderedKeys.index(of: key)
|
||||
}
|
||||
|
||||
public func elementAtIndex(index: Index) -> Element? {
|
||||
guard _orderedKeys.indices.contains(index) else { return nil }
|
||||
|
||||
let key = _orderedKeys[index]
|
||||
|
||||
guard let value = self._keysToValues[key] else {
|
||||
fatalError("Inconsistency error occured in OrderedDictionary")
|
||||
}
|
||||
|
||||
return (key, value)
|
||||
}
|
||||
|
||||
public func insertElementWithKey(key: Key, value: Value, atIndex index: Index) -> Value? {
|
||||
return insertElement(newElement: (key, value), atIndex: index)
|
||||
}
|
||||
|
||||
public func insertElement(newElement: Element, atIndex index: Index) -> Value? {
|
||||
guard index >= 0 else {
|
||||
fatalError("Negative OrderedDictionary index is out of range")
|
||||
}
|
||||
|
||||
guard index <= count else {
|
||||
fatalError("OrderedDictionary index out of range")
|
||||
}
|
||||
|
||||
let (key, value) = newElement
|
||||
|
||||
let adjustedIndex: Int
|
||||
let currentValue: Value?
|
||||
|
||||
if let currentIndex = _orderedKeys.index(of: key) {
|
||||
currentValue = _keysToValues[key]
|
||||
adjustedIndex = (currentIndex < index - 1) ? index - 1 : index
|
||||
|
||||
_orderedKeys.remove(at: currentIndex)
|
||||
_keysToValues[key] = nil
|
||||
} else {
|
||||
currentValue = nil
|
||||
adjustedIndex = index
|
||||
}
|
||||
|
||||
_orderedKeys.insert(key, at: adjustedIndex)
|
||||
_keysToValues[key] = value
|
||||
|
||||
return currentValue
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func updateElement(element: Element, atIndex index: Index) -> Element? {
|
||||
guard let currentElement = elementAtIndex(index: index) else {
|
||||
fatalError("OrderedDictionary index out of range")
|
||||
}
|
||||
|
||||
let (newKey, newValue) = element
|
||||
|
||||
_orderedKeys[index] = newKey
|
||||
_keysToValues[newKey] = newValue
|
||||
|
||||
return currentElement
|
||||
}
|
||||
|
||||
public func removeAtIndex(index: Index) -> Element? {
|
||||
if let element = elementAtIndex(index: index) {
|
||||
_orderedKeys.remove(at: index)
|
||||
_keysToValues.removeValue(forKey: element.0)
|
||||
|
||||
return element
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - CollectionType Conformance
|
||||
// ======================================================= //
|
||||
|
||||
public var startIndex: Index {
|
||||
return _orderedKeys.startIndex
|
||||
}
|
||||
|
||||
public var endIndex: Index {
|
||||
return _orderedKeys.endIndex
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Internal Backing Store
|
||||
// ======================================================= //
|
||||
|
||||
/// The backing store for the ordered keys.
|
||||
internal var _orderedKeys = [Key]()
|
||||
|
||||
/// The backing store for the mapping of keys to values.
|
||||
internal var _keysToValues = [Key: Value]()
|
||||
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Initializations from Literals
|
||||
// ======================================================= //
|
||||
|
||||
//extension OrderedDictionary: ExpressibleByArrayLiteral {
|
||||
//
|
||||
// public convenience init(arrayLiteral elements: Element...) {
|
||||
// self.init(elements: elements)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension OrderedDictionary: ExpressibleByDictionaryLiteral {
|
||||
//
|
||||
// public convenience init(dictionaryLiteral elements: Element...) {
|
||||
// self.init(elements: elements)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
extension OrderedDictionary: LazySequenceProtocol {
|
||||
|
||||
func generate() -> AnyIterator<Value> {
|
||||
var i = 0
|
||||
return AnyIterator {
|
||||
if (i >= self.orderedValues.count) {
|
||||
return nil
|
||||
}
|
||||
i += 1
|
||||
return self.orderedValues[i-1]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Description
|
||||
// ======================================================= //
|
||||
|
||||
extension OrderedDictionary: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
return constructDescription(debug: false)
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
return constructDescription(debug: true)
|
||||
}
|
||||
|
||||
private func constructDescription(debug: Bool) -> String {
|
||||
// The implementation of the description is inspired by zwaldowski's implementation of the ordered dictionary.
|
||||
// See http://bit.ly/1VL4JUR
|
||||
|
||||
if isEmpty { return "[:]" }
|
||||
|
||||
func descriptionForItem(item: Any) -> String {
|
||||
var description = ""
|
||||
|
||||
if debug {
|
||||
debugPrint(item, separator: "", terminator: "", to: &description)
|
||||
} else {
|
||||
print(item, separator: "", terminator: "", to: &description)
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
let bodyComponents = map({ (key: Key, value: Value) -> String in
|
||||
return descriptionForItem(item: key) + ": " + descriptionForItem(item: value)
|
||||
})
|
||||
|
||||
let body = bodyComponents.joined(separator: ", ")
|
||||
|
||||
return "[\(body)]"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OrderedDictionary: Equatable {
|
||||
/// 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: OrderedDictionary<Key, Value>, rhs: OrderedDictionary<Key, Value>) -> Bool {
|
||||
if(lhs.count != rhs.count) {return false}
|
||||
return (lhs._orderedKeys == rhs._orderedKeys) && (lhs._keysToValues == rhs._keysToValues)
|
||||
}
|
||||
}
|
||||
|
||||
//public func == <Key: Equatable, Value: Equatable>(lhs: OrderedDictionary<Key, Value>, rhs: OrderedDictionary<Key, Value>) -> Bool {
|
||||
// return lhs._orderedKeys == rhs._orderedKeys && lhs._keysToValues == rhs._keysToValues
|
||||
//}
|
||||
|
||||
/**
|
||||
* Elements IteratorProtocol.
|
||||
*/
|
||||
public struct OrderedDictionaryIterator<Key: Hashable, Value: Equatable>: IteratorProtocol {
|
||||
|
||||
/// Elements reference
|
||||
let orderedDictionary: OrderedDictionary<Key, Value>
|
||||
//current element index
|
||||
var index = 0
|
||||
|
||||
/// Initializer
|
||||
init(_ od: OrderedDictionary<Key, Value>) {
|
||||
self.orderedDictionary = od
|
||||
}
|
||||
|
||||
/// Advances to the next element and returns it, or `nil` if no next element
|
||||
mutating public func next() -> Value? {
|
||||
|
||||
let result = index < orderedDictionary.orderedKeys.count ? orderedDictionary[orderedDictionary.orderedKeys[index]] : nil
|
||||
index += 1
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elements Extension Sequence.
|
||||
*/
|
||||
extension OrderedDictionary: Sequence {
|
||||
/// Returns an iterator over the elements of this sequence.
|
||||
func generate()->OrderedDictionaryIterator<Key, Value> {
|
||||
return OrderedDictionaryIterator(self)
|
||||
}
|
||||
}
|
||||
Generated
+453
@@ -0,0 +1,453 @@
|
||||
//
|
||||
// OrderedSet.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 12/11/16.
|
||||
// Copyright © 2016 Nabil Chatbi. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
/// An ordered, unique collection of objects.
|
||||
public class OrderedSet<T: Hashable> {
|
||||
public typealias Index = Int
|
||||
fileprivate var contents = [T: Index]() // Needs to have a value of Index instead of Void for fast removals
|
||||
fileprivate var sequencedContents = Array<UnsafeMutablePointer<T>>()
|
||||
|
||||
/**
|
||||
Inititalizes an empty ordered set.
|
||||
- returns: An empty ordered set.
|
||||
*/
|
||||
public init() { }
|
||||
|
||||
deinit {
|
||||
removeAllObjects()
|
||||
}
|
||||
|
||||
/**
|
||||
Initializes a new ordered set with the order and contents
|
||||
of sequence.
|
||||
If an object appears more than once in the sequence it will only appear
|
||||
once in the ordered set, at the position of its first occurance.
|
||||
- parameter sequence: The sequence to initialize the ordered set with.
|
||||
- returns: An initialized ordered set with the contents of sequence.
|
||||
*/
|
||||
public init<S: Sequence>(sequence: S) where S.Iterator.Element == T {
|
||||
for object in sequence {
|
||||
if contents[object] == nil {
|
||||
contents[object] = contents.count
|
||||
|
||||
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
pointer.initialize(to: object)
|
||||
sequencedContents.append(pointer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public required init(arrayLiteral elements: T...) {
|
||||
for object in elements {
|
||||
if contents[object] == nil {
|
||||
contents[object] = contents.count
|
||||
|
||||
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
pointer.initialize(to: object)
|
||||
sequencedContents.append(pointer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Locate the index of an object in the ordered set.
|
||||
It is preferable to use this method over the global find() for performance reasons.
|
||||
- parameter object: The object to find the index for.
|
||||
- returns: The index of the object, or nil if the object is not in the ordered set.
|
||||
*/
|
||||
public func index(of object: T) -> Index? {
|
||||
if let index = contents[object] {
|
||||
return index
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Appends an object to the end of the ordered set.
|
||||
- parameter object: The object to be appended.
|
||||
*/
|
||||
public func append(_ object: T) {
|
||||
|
||||
if let lastIndex = index(of: object) {
|
||||
remove(object)
|
||||
insert(object, at: lastIndex)
|
||||
} else {
|
||||
contents[object] = contents.count
|
||||
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
pointer.initialize(to: object)
|
||||
sequencedContents.append(pointer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Appends a sequence of objects to the end of the ordered set.
|
||||
- parameter sequence: The sequence of objects to be appended.
|
||||
*/
|
||||
public func append<S: Sequence>(contentsOf sequence: S) where S.Iterator.Element == T {
|
||||
var gen = sequence.makeIterator()
|
||||
while let object: T = gen.next() {
|
||||
append(object)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes an object from the ordered set.
|
||||
If the object exists in the ordered set, it will be removed.
|
||||
If it is not the last object in the ordered set, subsequent
|
||||
objects will be shifted down one position.
|
||||
- parameter object: The object to be removed.
|
||||
*/
|
||||
public func remove(_ object: T) {
|
||||
if let index = contents[object] {
|
||||
contents[object] = nil
|
||||
#if !swift(>=4.1)
|
||||
sequencedContents[index].deallocate(capacity: 1)
|
||||
#else
|
||||
sequencedContents[index].deallocate()
|
||||
#endif
|
||||
|
||||
sequencedContents.remove(at: index)
|
||||
|
||||
for (object, i) in contents {
|
||||
if i < index {
|
||||
continue
|
||||
}
|
||||
|
||||
contents[object] = i - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the given objects from the ordered set.
|
||||
- parameter objects: The objects to be removed.
|
||||
*/
|
||||
public func remove<S: Sequence>(_ objects: S) where S.Iterator.Element == T {
|
||||
var gen = objects.makeIterator()
|
||||
while let object: T = gen.next() {
|
||||
remove(object)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes an object at a given index.
|
||||
This method will cause a fatal error if you attempt to move an object to an index that is out of bounds.
|
||||
- parameter index: The index of the object to be removed.
|
||||
*/
|
||||
public func removeObject(at index: Index) {
|
||||
if index < 0 || index >= count {
|
||||
fatalError("Attempting to remove an object at an index that does not exist")
|
||||
}
|
||||
|
||||
remove(sequencedContents[index].pointee)
|
||||
}
|
||||
|
||||
/**
|
||||
Removes all objects in the ordered set.
|
||||
*/
|
||||
public func removeAllObjects() {
|
||||
contents.removeAll()
|
||||
|
||||
for sequencedContent in sequencedContents {
|
||||
#if !swift(>=4.1)
|
||||
sequencedContent.deallocate(capacity: 1)
|
||||
#else
|
||||
sequencedContent.deallocate()
|
||||
#endif
|
||||
}
|
||||
sequencedContents.removeAll()
|
||||
}
|
||||
|
||||
/**
|
||||
Swaps two objects contained within the ordered set.
|
||||
Both objects must exist within the set, or the swap will not occur.
|
||||
- parameter first: The first object to be swapped.
|
||||
- parameter second: The second object to be swapped.
|
||||
*/
|
||||
public func swapObject(_ first: T, with second: T) {
|
||||
if let firstPosition = contents[first] {
|
||||
if let secondPosition = contents[second] {
|
||||
contents[first] = secondPosition
|
||||
contents[second] = firstPosition
|
||||
|
||||
sequencedContents[firstPosition].pointee = second
|
||||
sequencedContents[secondPosition].pointee = first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Tests if the ordered set contains any objects within a sequence.
|
||||
- parameter other: The sequence to look for the intersection in.
|
||||
- returns: Returns true if the sequence and set contain any equal objects, otherwise false.
|
||||
*/
|
||||
public func intersects<S: Sequence>(_ other: S) -> Bool where S.Iterator.Element == T {
|
||||
var gen = other.makeIterator()
|
||||
while let object: T = gen.next() {
|
||||
if contains(object) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
Tests if a the ordered set is a subset of another sequence.
|
||||
- parameter sequence: The sequence to check.
|
||||
- returns: true if the sequence contains all objects contained in the receiver, otherwise false.
|
||||
*/
|
||||
public func isSubset<S: Sequence>(of sequence: S) -> Bool where S.Iterator.Element == T {
|
||||
for (object, _) in contents {
|
||||
if !sequence.contains(object) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
Moves an object to a different index, shifting all objects in between the movement.
|
||||
This method is a no-op if the object doesn't exist in the set or the index is the
|
||||
same that the object is currently at.
|
||||
This method will cause a fatal error if you attempt to move an object to an index that is out of bounds.
|
||||
- parameter object: The object to be moved
|
||||
- parameter index: The index that the object should be moved to.
|
||||
*/
|
||||
public func moveObject(_ object: T, toIndex index: Index) {
|
||||
if index < 0 || index >= count {
|
||||
fatalError("Attempting to move an object at an index that does not exist")
|
||||
}
|
||||
|
||||
if let position = contents[object] {
|
||||
// Return if the client attempted to move to the current index
|
||||
if position == index {
|
||||
return
|
||||
}
|
||||
|
||||
let adjustment = position > index ? -1 : 1
|
||||
|
||||
var currentIndex = position
|
||||
while currentIndex != index {
|
||||
let nextIndex = currentIndex + adjustment
|
||||
|
||||
let firstObject = sequencedContents[currentIndex].pointee
|
||||
let secondObject = sequencedContents[nextIndex].pointee
|
||||
|
||||
sequencedContents[currentIndex].pointee = secondObject
|
||||
sequencedContents[nextIndex].pointee = firstObject
|
||||
|
||||
contents[firstObject] = nextIndex
|
||||
contents[secondObject] = currentIndex
|
||||
|
||||
currentIndex += adjustment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Moves an object from one index to a different index, shifting all objects in between the movement.
|
||||
This method is a no-op if the index is the same that the object is currently at.
|
||||
This method will cause a fatal error if you attempt to move an object fro man index that is out of bounds
|
||||
or to an index that is out of bounds.
|
||||
- parameter index: The index of the object to be moved.
|
||||
- parameter toIndex: The index that the object should be moved to.
|
||||
*/
|
||||
public func moveObject(at index: Index, to toIndex: Index) {
|
||||
if ((index < 0 || index >= count) || (toIndex < 0 || toIndex >= count)) {
|
||||
fatalError("Attempting to move an object at or to an index that does not exist")
|
||||
}
|
||||
|
||||
moveObject(self[index], toIndex: toIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
Inserts an object at a given index, shifting all objects above it up one.
|
||||
This method will cause a fatal error if you attempt to insert the object out of bounds.
|
||||
If the object already exists in the OrderedSet, this operation is a no-op.
|
||||
- parameter object: The object to be inserted.
|
||||
- parameter index: The index to be inserted at.
|
||||
*/
|
||||
public func insert(_ object: T, at index: Index) {
|
||||
if index > count || index < 0 {
|
||||
fatalError("Attempting to insert an object at an index that does not exist")
|
||||
}
|
||||
|
||||
if contents[object] != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Append our object, then swap them until its at the end.
|
||||
append(object)
|
||||
|
||||
for i in (index..<count-1).reversed() {
|
||||
swapObject(self[i], with: self[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Inserts objects at a given index, shifting all objects above it up one.
|
||||
This method will cause a fatal error if you attempt to insert the objects out of bounds.
|
||||
If an object in objects already exists in the OrderedSet it will not be added. Objects that occur twice
|
||||
in the sequence will only be added once.
|
||||
- parameter objects: The objects to be inserted.
|
||||
- parameter index: The index to be inserted at.
|
||||
*/
|
||||
public func insert<S: Sequence>(_ objects: S, at index: Index) where S.Iterator.Element == T {
|
||||
if index > count || index < 0 {
|
||||
fatalError("Attempting to insert an object at an index that does not exist")
|
||||
}
|
||||
|
||||
var addedObjectCount = 0
|
||||
|
||||
for object in objects {
|
||||
if contents[object] == nil {
|
||||
let seqIdx = index + addedObjectCount
|
||||
let element = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
element.initialize(to: object)
|
||||
sequencedContents.insert(element, at: seqIdx)
|
||||
contents[object] = seqIdx
|
||||
addedObjectCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Now we'll remove duplicates and update the shifted objects position in the contents
|
||||
// dictionary.
|
||||
for i in index + addedObjectCount..<count {
|
||||
contents[sequencedContents[i].pointee] = i
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the last object in the set, or `nil` if the set is empty.
|
||||
public var last: T? {
|
||||
return sequencedContents.last?.pointee
|
||||
}
|
||||
}
|
||||
|
||||
extension OrderedSet: ExpressibleByArrayLiteral { }
|
||||
|
||||
extension OrderedSet where T: Comparable {}
|
||||
|
||||
extension OrderedSet {
|
||||
|
||||
public var count: Int {
|
||||
return contents.count
|
||||
}
|
||||
|
||||
public var isEmpty: Bool {
|
||||
return count == 0
|
||||
}
|
||||
|
||||
public var first: T? {
|
||||
guard count > 0 else { return nil }
|
||||
return sequencedContents[0].pointee
|
||||
}
|
||||
|
||||
public func index(after i: Int) -> Int {
|
||||
return sequencedContents.index(after: i)
|
||||
}
|
||||
|
||||
public var startIndex: Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
public var endIndex: Int {
|
||||
return contents.count
|
||||
}
|
||||
|
||||
public subscript(index: Index) -> T {
|
||||
get {
|
||||
return sequencedContents[index].pointee
|
||||
}
|
||||
|
||||
set {
|
||||
let previousCount = contents.count
|
||||
contents[sequencedContents[index].pointee] = nil
|
||||
contents[newValue] = index
|
||||
|
||||
// If the count is reduced we used an existing value, and need to sync up sequencedContents
|
||||
if contents.count == previousCount {
|
||||
sequencedContents[index].pointee = newValue
|
||||
} else {
|
||||
sequencedContents.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OrderedSet: Sequence {
|
||||
public typealias Iterator = OrderedSetGenerator<T>
|
||||
|
||||
public func makeIterator() -> Iterator {
|
||||
return OrderedSetGenerator(set: self)
|
||||
}
|
||||
}
|
||||
|
||||
public struct OrderedSetGenerator<T: Hashable>: IteratorProtocol {
|
||||
public typealias Element = T
|
||||
private var generator: IndexingIterator<Array<UnsafeMutablePointer<T>>>
|
||||
|
||||
public init(set: OrderedSet<T>) {
|
||||
generator = set.sequencedContents.makeIterator()
|
||||
}
|
||||
|
||||
public mutating func next() -> Element? {
|
||||
return generator.next()?.pointee
|
||||
}
|
||||
}
|
||||
|
||||
extension OrderedSetGenerator where T: Comparable {}
|
||||
|
||||
public func +<T, S: Sequence> (lhs: OrderedSet<T>, rhs: S) -> OrderedSet<T> where S.Iterator.Element == T {
|
||||
let joinedSet = lhs
|
||||
joinedSet.append(contentsOf: rhs)
|
||||
|
||||
return joinedSet
|
||||
}
|
||||
|
||||
public func +=<T, S: Sequence> (lhs: inout OrderedSet<T>, rhs: S) where S.Iterator.Element == T {
|
||||
lhs.append(contentsOf: rhs)
|
||||
}
|
||||
|
||||
public func -<T, S: Sequence> (lhs: OrderedSet<T>, rhs: S) -> OrderedSet<T> where S.Iterator.Element == T {
|
||||
let purgedSet = lhs
|
||||
purgedSet.remove(rhs)
|
||||
|
||||
return purgedSet
|
||||
}
|
||||
|
||||
public func -=<T, S: Sequence> (lhs: inout OrderedSet<T>, rhs: S) where S.Iterator.Element == T {
|
||||
lhs.remove(rhs)
|
||||
}
|
||||
|
||||
extension OrderedSet: Equatable { }
|
||||
|
||||
public func ==<T> (lhs: OrderedSet<T>, rhs: OrderedSet<T>) -> Bool {
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
|
||||
for object in lhs {
|
||||
if lhs.contents[object] != rhs.contents[object] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
extension OrderedSet: CustomStringConvertible {
|
||||
public var description: String {
|
||||
let children = map({ "\($0)" }).joined(separator: ", ")
|
||||
return "OrderedSet (\(count) object(s)): [\(children)]"
|
||||
}
|
||||
}
|
||||
Generated
+42
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// ParseError.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 19/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* A Parse Error records an error in the input HTML that occurs in either the tokenisation or the tree building phase.
|
||||
*/
|
||||
open class ParseError {
|
||||
private let pos: Int
|
||||
private let errorMsg: String
|
||||
|
||||
init(_ pos: Int, _ errorMsg: String) {
|
||||
self.pos = pos
|
||||
self.errorMsg = errorMsg
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the error message.
|
||||
* @return the error message.
|
||||
*/
|
||||
open func getErrorMessage() -> String {
|
||||
return errorMsg
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the offset of the error.
|
||||
* @return error offset within input
|
||||
*/
|
||||
open func getPosition() -> Int {
|
||||
return pos
|
||||
}
|
||||
|
||||
open func toString() -> String {
|
||||
return "\(pos): " + errorMsg
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// ParseErrorList.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 19/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class ParseErrorList {
|
||||
private static let INITIAL_CAPACITY: Int = 16
|
||||
private let maxSize: Int
|
||||
private let initialCapacity: Int
|
||||
private var array: Array<ParseError?> = Array<ParseError>()
|
||||
|
||||
init(_ initialCapacity: Int, _ maxSize: Int) {
|
||||
self.maxSize = maxSize
|
||||
self.initialCapacity = initialCapacity
|
||||
array = Array(repeating: nil, count: maxSize)
|
||||
}
|
||||
|
||||
func canAddError() -> Bool {
|
||||
return array.count < maxSize
|
||||
}
|
||||
|
||||
func getMaxSize() -> Int {
|
||||
return maxSize
|
||||
}
|
||||
|
||||
static func noTracking() -> ParseErrorList {
|
||||
return ParseErrorList(0, 0)
|
||||
}
|
||||
|
||||
static func tracking(_ maxSize: Int) -> ParseErrorList {
|
||||
return ParseErrorList(INITIAL_CAPACITY, maxSize)
|
||||
}
|
||||
|
||||
// // you need to provide the Equatable functionality
|
||||
// static func ==(leftFoo: Foo, rightFoo: Foo) -> Bool {
|
||||
// return ObjectIdentifier(leftFoo) == ObjectIdentifier(rightFoo)
|
||||
// }
|
||||
|
||||
open func add(_ e: ParseError) {
|
||||
array.append(e)
|
||||
}
|
||||
|
||||
open func add(_ index: Int, _ element: ParseError) {
|
||||
array.insert(element, at: index)
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// ParseSettings.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 14/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class ParseSettings {
|
||||
/**
|
||||
* HTML default settings: both tag and attribute names are lower-cased during parsing.
|
||||
*/
|
||||
public static let htmlDefault: ParseSettings = ParseSettings(false, false)
|
||||
/**
|
||||
* Preserve both tag and attribute case.
|
||||
*/
|
||||
public static let preserveCase: ParseSettings = ParseSettings(true, true)
|
||||
|
||||
private let preserveTagCase: Bool
|
||||
private let preserveAttributeCase: Bool
|
||||
|
||||
/**
|
||||
* Define parse settings.
|
||||
* @param tag preserve tag case?
|
||||
* @param attribute preserve attribute name case?
|
||||
*/
|
||||
public init(_ tag: Bool, _ attribute: Bool) {
|
||||
preserveTagCase = tag
|
||||
preserveAttributeCase = attribute
|
||||
}
|
||||
|
||||
open func normalizeTag(_ name: String) -> String {
|
||||
var name = name.trim()
|
||||
if (!preserveTagCase) {
|
||||
name = name.lowercased()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
open func normalizeAttribute(_ name: String) -> String {
|
||||
var name = name.trim()
|
||||
if (!preserveAttributeCase) {
|
||||
name = name.lowercased()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
open func normalizeAttributes(_ attributes: Attributes)throws ->Attributes {
|
||||
if (!preserveAttributeCase) {
|
||||
for attr in attributes {
|
||||
try attr.setKey(key: attr.getKey().lowercased())
|
||||
}
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+201
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// Parser.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Parses HTML into a {@link org.jsoup.nodes.Document}. Generally best to use one of the more convenient parse methods
|
||||
* in {@link org.jsoup.Jsoup}.
|
||||
*/
|
||||
public class Parser {
|
||||
private static let DEFAULT_MAX_ERRORS: Int = 0 // by default, error tracking is disabled.
|
||||
|
||||
private var _treeBuilder: TreeBuilder
|
||||
private var _maxErrors: Int = DEFAULT_MAX_ERRORS
|
||||
private var _errors: ParseErrorList = ParseErrorList(16, 16)
|
||||
private var _settings: ParseSettings
|
||||
|
||||
/**
|
||||
* Create a new Parser, using the specified TreeBuilder
|
||||
* @param treeBuilder TreeBuilder to use to parse input into Documents.
|
||||
*/
|
||||
init(_ treeBuilder: TreeBuilder) {
|
||||
self._treeBuilder = treeBuilder
|
||||
_settings = treeBuilder.defaultSettings()
|
||||
}
|
||||
|
||||
public func parseInput(_ html: String, _ baseUri: String)throws->Document {
|
||||
_errors = isTrackErrors() ? ParseErrorList.tracking(_maxErrors) : ParseErrorList.noTracking()
|
||||
return try _treeBuilder.parse(html, baseUri, _errors, _settings)
|
||||
}
|
||||
|
||||
// gets & sets
|
||||
/**
|
||||
* Get the TreeBuilder currently in use.
|
||||
* @return current TreeBuilder.
|
||||
*/
|
||||
public func getTreeBuilder() -> TreeBuilder {
|
||||
return _treeBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the TreeBuilder used when parsing content.
|
||||
* @param treeBuilder current TreeBuilder
|
||||
* @return this, for chaining
|
||||
*/
|
||||
@discardableResult
|
||||
public func setTreeBuilder(_ treeBuilder: TreeBuilder) -> Parser {
|
||||
self._treeBuilder = treeBuilder
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if parse error tracking is enabled.
|
||||
* @return current track error state.
|
||||
*/
|
||||
public func isTrackErrors() -> Bool {
|
||||
return _maxErrors > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable parse error tracking for the next parse.
|
||||
* @param maxErrors the maximum number of errors to track. Set to 0 to disable.
|
||||
* @return this, for chaining
|
||||
*/
|
||||
@discardableResult
|
||||
public func setTrackErrors(_ maxErrors: Int) -> Parser {
|
||||
self._maxErrors = maxErrors
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the parse errors, if any, from the last parse.
|
||||
* @return list of parse errors, up to the size of the maximum errors tracked.
|
||||
*/
|
||||
public func getErrors() -> ParseErrorList {
|
||||
return _errors
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func settings(_ settings: ParseSettings) -> Parser {
|
||||
self._settings = settings
|
||||
return self
|
||||
}
|
||||
|
||||
public func settings() -> ParseSettings {
|
||||
return _settings
|
||||
}
|
||||
|
||||
// static parse functions below
|
||||
/**
|
||||
* Parse HTML into a Document.
|
||||
*
|
||||
* @param html HTML to parse
|
||||
* @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
|
||||
*
|
||||
* @return parsed Document
|
||||
*/
|
||||
public static func parse(_ html: String, _ baseUri: String)throws->Document {
|
||||
let treeBuilder: TreeBuilder = HtmlTreeBuilder()
|
||||
return try treeBuilder.parse(html, baseUri, ParseErrorList.noTracking(), treeBuilder.defaultSettings())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a fragment of HTML into a list of nodes. The context element, if supplied, supplies parsing context.
|
||||
*
|
||||
* @param fragmentHtml the fragment of HTML to parse
|
||||
* @param context (optional) the element that this HTML fragment is being parsed for (i.e. for inner HTML). This
|
||||
* provides stack context (for implicit element creation).
|
||||
* @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
|
||||
*
|
||||
* @return list of nodes parsed from the input HTML. Note that the context element, if supplied, is not modified.
|
||||
*/
|
||||
public static func parseFragment(_ fragmentHtml: String, _ context: Element?, _ baseUri: String)throws->Array<Node> {
|
||||
let treeBuilder = HtmlTreeBuilder()
|
||||
return try treeBuilder.parseFragment(fragmentHtml, context, baseUri, ParseErrorList.noTracking(), treeBuilder.defaultSettings())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a fragment of XML into a list of nodes.
|
||||
*
|
||||
* @param fragmentXml the fragment of XML to parse
|
||||
* @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
|
||||
* @return list of nodes parsed from the input XML.
|
||||
*/
|
||||
public static func parseXmlFragment(_ fragmentXml: String, _ baseUri: String)throws->Array<Node> {
|
||||
let treeBuilder: XmlTreeBuilder = XmlTreeBuilder()
|
||||
return try treeBuilder.parseFragment(fragmentXml, baseUri, ParseErrorList.noTracking(), treeBuilder.defaultSettings())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a fragment of HTML into the {@code body} of a Document.
|
||||
*
|
||||
* @param bodyHtml fragment of HTML
|
||||
* @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
|
||||
*
|
||||
* @return Document, with empty head, and HTML parsed into body
|
||||
*/
|
||||
public static func parseBodyFragment(_ bodyHtml: String, _ baseUri: String)throws->Document {
|
||||
let doc: Document = Document.createShell(baseUri)
|
||||
if let body: Element = doc.body() {
|
||||
let nodeList: Array<Node> = try parseFragment(bodyHtml, body, baseUri)
|
||||
//var nodes: [Node] = nodeList.toArray(Node[nodeList.size()]) // the node list gets modified when re-parented
|
||||
if nodeList.count > 0 {
|
||||
for i in 1..<nodeList.count {
|
||||
try nodeList[i].remove()
|
||||
}
|
||||
}
|
||||
for node: Node in nodeList {
|
||||
try body.appendChild(node)
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to unescape HTML entities from a string
|
||||
* @param string HTML escaped string
|
||||
* @param inAttribute if the string is to be escaped in strict mode (as attributes are)
|
||||
* @return an unescaped string
|
||||
*/
|
||||
public static func unescapeEntities(_ string: String, _ inAttribute: Bool)throws->String {
|
||||
let tokeniser: Tokeniser = Tokeniser(CharacterReader(string), ParseErrorList.noTracking())
|
||||
return try tokeniser.unescapeEntities(inAttribute)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bodyHtml HTML to parse
|
||||
* @param baseUri baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
|
||||
*
|
||||
* @return parsed Document
|
||||
* @deprecated Use {@link #parseBodyFragment} or {@link #parseFragment} instead.
|
||||
*/
|
||||
public static func parseBodyFragmentRelaxed(_ bodyHtml: String, _ baseUri: String)throws->Document {
|
||||
return try parse(bodyHtml, baseUri)
|
||||
}
|
||||
|
||||
// builders
|
||||
|
||||
/**
|
||||
* Create a new HTML parser. This parser treats input as HTML5, and enforces the creation of a normalised document,
|
||||
* based on a knowledge of the semantics of the incoming tags.
|
||||
* @return a new HTML parser.
|
||||
*/
|
||||
public static func htmlParser() -> Parser {
|
||||
return Parser(HtmlTreeBuilder())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new XML parser. This parser assumes no knowledge of the incoming tags and does not treat it as HTML,
|
||||
* rather creates a simple tree directly from the input.
|
||||
* @return a new simple XML parser.
|
||||
*/
|
||||
public static func xmlParser() -> Parser {
|
||||
return Parser(XmlTreeBuilder())
|
||||
}
|
||||
}
|
||||
Generated
+84
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Regex.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 08/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Pattern {
|
||||
public static let CASE_INSENSITIVE: Int = 0x02
|
||||
let pattern: String
|
||||
|
||||
init(_ pattern: String) {
|
||||
self.pattern = pattern
|
||||
}
|
||||
|
||||
static public func compile(_ s: String) -> Pattern {
|
||||
return Pattern(s)
|
||||
}
|
||||
static public func compile(_ s: String, _ op: Int) -> Pattern {
|
||||
return Pattern(s)
|
||||
}
|
||||
|
||||
func validate()throws {
|
||||
_ = try NSRegularExpression(pattern: self.pattern, options: [])
|
||||
}
|
||||
|
||||
public func matcher(in text: String) -> Matcher {
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: self.pattern, options: [])
|
||||
let nsString = NSString(string: text)
|
||||
let results = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
|
||||
|
||||
return Matcher(results, text)
|
||||
} catch let error {
|
||||
print("invalid regex: \(error.localizedDescription)")
|
||||
return Matcher([], text)
|
||||
}
|
||||
}
|
||||
|
||||
public func toString() -> String {
|
||||
return pattern
|
||||
}
|
||||
}
|
||||
|
||||
public class Matcher {
|
||||
let matches: [NSTextCheckingResult]
|
||||
let string: String
|
||||
var index: Int = -1
|
||||
|
||||
public var count: Int { return matches.count}
|
||||
|
||||
init(_ m: [NSTextCheckingResult], _ s: String) {
|
||||
matches = m
|
||||
string = s
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func find() -> Bool {
|
||||
index += 1
|
||||
if(index < matches.count) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func group(_ i: Int) -> String? {
|
||||
let b = matches[index]
|
||||
#if !os(Linux) && !swift(>=4)
|
||||
let c = b.rangeAt(i)
|
||||
#else
|
||||
let c = b.range(at: i)
|
||||
#endif
|
||||
|
||||
if(c.location == NSNotFound) {return nil}
|
||||
let result = string.substring(c.location, c.length)
|
||||
return result
|
||||
}
|
||||
public func group() -> String? {
|
||||
return group(0)
|
||||
}
|
||||
}
|
||||
Generated
+318
@@ -0,0 +1,318 @@
|
||||
//
|
||||
// QueryParser.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 23/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Parses a CSS selector into an Evaluator tree.
|
||||
*/
|
||||
public class QueryParser {
|
||||
private static let combinators: [String] = [",", ">", "+", "~", " "]
|
||||
private static let AttributeEvals: [String] = ["=", "!=", "^=", "$=", "*=", "~="]
|
||||
|
||||
private var tq: TokenQueue
|
||||
private var query: String
|
||||
private var evals: Array<Evaluator> = Array<Evaluator>()
|
||||
|
||||
/**
|
||||
* Create a new QueryParser.
|
||||
* @param query CSS query
|
||||
*/
|
||||
private init(_ query: String) {
|
||||
self.query = query
|
||||
self.tq = TokenQueue(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CSS query into an Evaluator.
|
||||
* @param query CSS query
|
||||
* @return Evaluator
|
||||
*/
|
||||
public static func parse(_ query: String)throws->Evaluator {
|
||||
let p = QueryParser(query)
|
||||
return try p.parse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the query
|
||||
* @return Evaluator
|
||||
*/
|
||||
public func parse()throws->Evaluator {
|
||||
tq.consumeWhitespace()
|
||||
|
||||
if (tq.matchesAny(QueryParser.combinators)) { // if starts with a combinator, use root as elements
|
||||
evals.append( StructuralEvaluator.Root())
|
||||
try combinator(tq.consume())
|
||||
} else {
|
||||
try findElements()
|
||||
}
|
||||
|
||||
while (!tq.isEmpty()) {
|
||||
// hierarchy and extras
|
||||
let seenWhite: Bool = tq.consumeWhitespace()
|
||||
|
||||
if (tq.matchesAny(QueryParser.combinators)) {
|
||||
try combinator(tq.consume())
|
||||
} else if (seenWhite) {
|
||||
try combinator(" " as Character)
|
||||
} else { // E.class, E#id, E[attr] etc. AND
|
||||
try findElements() // take next el, #. etc off queue
|
||||
}
|
||||
}
|
||||
|
||||
if (evals.count == 1) {
|
||||
return evals[0]
|
||||
}
|
||||
return CombiningEvaluator.And(evals)
|
||||
}
|
||||
|
||||
private func combinator(_ combinator: Character)throws {
|
||||
tq.consumeWhitespace()
|
||||
let subQuery: String = consumeSubQuery() // support multi > childs
|
||||
|
||||
var rootEval: Evaluator? // the new topmost evaluator
|
||||
var currentEval: Evaluator? // the evaluator the new eval will be combined to. could be root, or rightmost or.
|
||||
let newEval: Evaluator = try QueryParser.parse(subQuery) // the evaluator to add into target evaluator
|
||||
var replaceRightMost: Bool = false
|
||||
|
||||
if (evals.count == 1) {
|
||||
currentEval = evals[0]
|
||||
rootEval = currentEval
|
||||
// make sure OR (,) has precedence:
|
||||
if (((rootEval as? CombiningEvaluator.Or) != nil) && combinator != ",") {
|
||||
currentEval = (currentEval as! CombiningEvaluator.Or).rightMostEvaluator()
|
||||
replaceRightMost = true
|
||||
}
|
||||
} else {
|
||||
currentEval = CombiningEvaluator.And(evals)
|
||||
rootEval = currentEval
|
||||
}
|
||||
evals.removeAll()
|
||||
|
||||
// for most combinators: change the current eval into an AND of the current eval and the new eval
|
||||
if (combinator == ">") {currentEval = CombiningEvaluator.And(newEval, StructuralEvaluator.ImmediateParent(currentEval!))} else if (combinator == " ") {currentEval = CombiningEvaluator.And(newEval, StructuralEvaluator.Parent(currentEval!))} else if (combinator == "+") {currentEval = CombiningEvaluator.And(newEval, StructuralEvaluator.ImmediatePreviousSibling(currentEval!))} else if (combinator == "~") {currentEval = CombiningEvaluator.And(newEval, StructuralEvaluator.PreviousSibling(currentEval!))} else if (combinator == ",") { // group or.
|
||||
let or: CombiningEvaluator.Or
|
||||
if ((currentEval as? CombiningEvaluator.Or) != nil) {
|
||||
or = currentEval as! CombiningEvaluator.Or
|
||||
or.add(newEval)
|
||||
} else {
|
||||
or = CombiningEvaluator.Or()
|
||||
or.add(currentEval!)
|
||||
or.add(newEval)
|
||||
}
|
||||
currentEval = or
|
||||
} else {
|
||||
throw Exception.Error(type: ExceptionType.SelectorParseException, Message: "Unknown combinator: \(String(combinator))")
|
||||
}
|
||||
|
||||
if (replaceRightMost) {
|
||||
(rootEval as! CombiningEvaluator.Or).replaceRightMostEvaluator(currentEval!)
|
||||
} else {
|
||||
rootEval = currentEval
|
||||
}
|
||||
evals.append(rootEval!)
|
||||
}
|
||||
|
||||
private func consumeSubQuery() -> String {
|
||||
let sq: StringBuilder = StringBuilder()
|
||||
while (!tq.isEmpty()) {
|
||||
if (tq.matches("(")) {
|
||||
sq.append("(").append(tq.chompBalanced("(", ")")).append(")")
|
||||
} else if (tq.matches("[")) {
|
||||
sq.append("[").append(tq.chompBalanced("[", "]")).append("]")
|
||||
} else if (tq.matchesAny(QueryParser.combinators)) {
|
||||
break
|
||||
} else {
|
||||
sq.append(tq.consume())
|
||||
}
|
||||
}
|
||||
return sq.toString()
|
||||
}
|
||||
|
||||
private func findElements()throws {
|
||||
if (tq.matchChomp("#")) {
|
||||
try byId()
|
||||
} else if (tq.matchChomp(".")) {
|
||||
try byClass()} else if (tq.matchesWord() || tq.matches("*|")) {try byTag()} else if (tq.matches("[")) {try byAttribute()} else if (tq.matchChomp("*")) { allElements()} else if (tq.matchChomp(":lt(")) {try indexLessThan()} else if (tq.matchChomp(":gt(")) {try indexGreaterThan()} else if (tq.matchChomp(":eq(")) {try indexEquals()} else if (tq.matches(":has(")) {try has()} else if (tq.matches(":contains(")) {try contains(false)} else if (tq.matches(":containsOwn(")) {try contains(true)} else if (tq.matches(":matches(")) {try matches(false)} else if (tq.matches(":matchesOwn(")) {try matches(true)} else if (tq.matches(":not(")) {try not()} else if (tq.matchChomp(":nth-child(")) {try cssNthChild(false, false)} else if (tq.matchChomp(":nth-last-child(")) {try cssNthChild(true, false)} else if (tq.matchChomp(":nth-of-type(")) {try cssNthChild(false, true)} else if (tq.matchChomp(":nth-last-of-type(")) {try cssNthChild(true, true)} else if (tq.matchChomp(":first-child")) {evals.append(Evaluator.IsFirstChild())} else if (tq.matchChomp(":last-child")) {evals.append(Evaluator.IsLastChild())} else if (tq.matchChomp(":first-of-type")) {evals.append(Evaluator.IsFirstOfType())} else if (tq.matchChomp(":last-of-type")) {evals.append(Evaluator.IsLastOfType())} else if (tq.matchChomp(":only-child")) {evals.append(Evaluator.IsOnlyChild())} else if (tq.matchChomp(":only-of-type")) {evals.append(Evaluator.IsOnlyOfType())} else if (tq.matchChomp(":empty")) {evals.append(Evaluator.IsEmpty())} else if (tq.matchChomp(":root")) {evals.append(Evaluator.IsRoot())} else // unhandled
|
||||
{
|
||||
throw Exception.Error(type: ExceptionType.SelectorParseException, Message: "Could not parse query \(query): unexpected token at \(tq.remainder())")
|
||||
}
|
||||
}
|
||||
|
||||
private func byId()throws {
|
||||
let id: String = tq.consumeCssIdentifier()
|
||||
try Validate.notEmpty(string: id)
|
||||
evals.append(Evaluator.Id(id))
|
||||
}
|
||||
|
||||
private func byClass()throws {
|
||||
let className: String = tq.consumeCssIdentifier()
|
||||
try Validate.notEmpty(string: className)
|
||||
evals.append(Evaluator.Class(className.trim()))
|
||||
}
|
||||
|
||||
private func byTag()throws {
|
||||
var tagName = tq.consumeElementSelector()
|
||||
|
||||
try Validate.notEmpty(string: tagName)
|
||||
|
||||
// namespaces: wildcard match equals(tagName) or ending in ":"+tagName
|
||||
if (tagName.startsWith("*|")) {
|
||||
evals.append(
|
||||
CombiningEvaluator.Or(
|
||||
Evaluator.Tag(tagName.trim().lowercased()),
|
||||
Evaluator.TagEndsWith(tagName.replacingOccurrences(of: "*|", with: ":").trim().lowercased())))
|
||||
} else {
|
||||
// namespaces: if element name is "abc:def", selector must be "abc|def", so flip:
|
||||
if (tagName.contains("|")) {
|
||||
tagName = tagName.replacingOccurrences(of: "|", with: ":")
|
||||
}
|
||||
|
||||
evals.append(Evaluator.Tag(tagName.trim()))
|
||||
}
|
||||
}
|
||||
|
||||
private func byAttribute()throws {
|
||||
let cq: TokenQueue = TokenQueue(tq.chompBalanced("[", "]")) // content queue
|
||||
let key: String = cq.consumeToAny(QueryParser.AttributeEvals) // eq, not, start, end, contain, match, (no val)
|
||||
try Validate.notEmpty(string: key)
|
||||
cq.consumeWhitespace()
|
||||
|
||||
if (cq.isEmpty()) {
|
||||
if (key.startsWith("^")) {
|
||||
evals.append(try Evaluator.AttributeStarting(key.substring(1)))
|
||||
} else {
|
||||
evals.append(Evaluator.Attribute(key))
|
||||
}
|
||||
} else {
|
||||
if (cq.matchChomp("=")) {
|
||||
evals.append(try Evaluator.AttributeWithValue(key, cq.remainder()))
|
||||
} else if (cq.matchChomp("!=")) {
|
||||
evals.append(try Evaluator.AttributeWithValueNot(key, cq.remainder()))
|
||||
} else if (cq.matchChomp("^=")) {
|
||||
evals.append(try Evaluator.AttributeWithValueStarting(key, cq.remainder()))
|
||||
} else if (cq.matchChomp("$=")) {
|
||||
evals.append(try Evaluator.AttributeWithValueEnding(key, cq.remainder()))
|
||||
} else if (cq.matchChomp("*=")) {
|
||||
evals.append(try Evaluator.AttributeWithValueContaining(key, cq.remainder()))
|
||||
} else if (cq.matchChomp("~=")) {
|
||||
evals.append( Evaluator.AttributeWithValueMatching(key, Pattern.compile(cq.remainder())))
|
||||
} else {
|
||||
throw Exception.Error(type: ExceptionType.SelectorParseException, Message: "Could not parse attribute query '\(query)': unexpected token at '\(cq.remainder())'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func allElements() {
|
||||
evals.append(Evaluator.AllElements())
|
||||
}
|
||||
|
||||
// pseudo selectors :lt, :gt, :eq
|
||||
private func indexLessThan()throws {
|
||||
evals.append(Evaluator.IndexLessThan(try consumeIndex()))
|
||||
}
|
||||
|
||||
private func indexGreaterThan()throws {
|
||||
evals.append(Evaluator.IndexGreaterThan(try consumeIndex()))
|
||||
}
|
||||
|
||||
private func indexEquals()throws {
|
||||
evals.append(Evaluator.IndexEquals(try consumeIndex()))
|
||||
}
|
||||
|
||||
//pseudo selectors :first-child, :last-child, :nth-child, ...
|
||||
private static let NTH_AB: Pattern = Pattern.compile("((\\+|-)?(\\d+)?)n(\\s*(\\+|-)?\\s*\\d+)?", Pattern.CASE_INSENSITIVE)
|
||||
private static let NTH_B: Pattern = Pattern.compile("(\\+|-)?(\\d+)")
|
||||
|
||||
private func cssNthChild(_ backwards: Bool, _ ofType: Bool)throws {
|
||||
let argS: String = tq.chompTo(")").trim().lowercased()
|
||||
let mAB: Matcher = QueryParser.NTH_AB.matcher(in: argS)
|
||||
let mB: Matcher = QueryParser.NTH_B.matcher(in: argS)
|
||||
var a: Int
|
||||
var b: Int
|
||||
if ("odd"==argS) {
|
||||
a = 2
|
||||
b = 1
|
||||
} else if ("even"==argS) {
|
||||
a = 2
|
||||
b = 0
|
||||
} else if (mAB.matches.count > 0) {
|
||||
mAB.find()
|
||||
a = mAB.group(3) != nil ? Int(mAB.group(1)!.replaceFirst(of: "^\\+", with: ""))! : 1
|
||||
b = mAB.group(4) != nil ? Int(mAB.group(4)!.replaceFirst(of: "^\\+", with: ""))! : 0
|
||||
} else if (mB.matches.count > 0) {
|
||||
a = 0
|
||||
mB.find()
|
||||
b = Int(mB.group()!.replaceFirst(of: "^\\+", with: ""))!
|
||||
} else {
|
||||
throw Exception.Error(type: ExceptionType.SelectorParseException, Message: "Could not parse nth-index '\(argS)': unexpected format")
|
||||
}
|
||||
if (ofType) {
|
||||
if (backwards) {
|
||||
evals.append(Evaluator.IsNthLastOfType(a, b))
|
||||
} else {
|
||||
evals.append(Evaluator.IsNthOfType(a, b))
|
||||
}
|
||||
} else {
|
||||
if (backwards) {
|
||||
evals.append(Evaluator.IsNthLastChild(a, b))
|
||||
} else {
|
||||
evals.append(Evaluator.IsNthChild(a, b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func consumeIndex()throws->Int {
|
||||
let indexS: String = tq.chompTo(")").trim()
|
||||
try Validate.isTrue(val: StringUtil.isNumeric(indexS), msg: "Index must be numeric")
|
||||
return Int(indexS)!
|
||||
}
|
||||
|
||||
// pseudo selector :has(el)
|
||||
private func has()throws {
|
||||
try tq.consume(":has")
|
||||
let subQuery: String = tq.chompBalanced("(", ")")
|
||||
try Validate.notEmpty(string: subQuery, msg: ":has(el) subselect must not be empty")
|
||||
evals.append(StructuralEvaluator.Has(try QueryParser.parse(subQuery)))
|
||||
}
|
||||
|
||||
// pseudo selector :contains(text), containsOwn(text)
|
||||
private func contains(_ own: Bool)throws {
|
||||
try tq.consume(own ? ":containsOwn" : ":contains")
|
||||
let searchText: String = TokenQueue.unescape(tq.chompBalanced("(", ")"))
|
||||
try Validate.notEmpty(string: searchText, msg: ":contains(text) query must not be empty")
|
||||
if (own) {
|
||||
evals.append(Evaluator.ContainsOwnText(searchText))
|
||||
} else {
|
||||
evals.append(Evaluator.ContainsText(searchText))
|
||||
}
|
||||
}
|
||||
|
||||
// :matches(regex), matchesOwn(regex)
|
||||
private func matches(_ own: Bool)throws {
|
||||
try tq.consume(own ? ":matchesOwn" : ":matches")
|
||||
let regex: String = tq.chompBalanced("(", ")") // don't unescape, as regex bits will be escaped
|
||||
try Validate.notEmpty(string: regex, msg: ":matches(regex) query must not be empty")
|
||||
|
||||
if (own) {
|
||||
evals.append(Evaluator.MatchesOwn(Pattern.compile(regex)))
|
||||
} else {
|
||||
evals.append(Evaluator.Matches(Pattern.compile(regex)))
|
||||
}
|
||||
}
|
||||
|
||||
// :not(selector)
|
||||
private func not()throws {
|
||||
try tq.consume(":not")
|
||||
let subQuery: String = tq.chompBalanced("(", ")")
|
||||
try Validate.notEmpty(string: subQuery, msg: ":not(selector) subselect must not be empty")
|
||||
|
||||
evals.append(StructuralEvaluator.Not(try QueryParser.parse(subQuery)))
|
||||
}
|
||||
|
||||
}
|
||||
Generated
+163
@@ -0,0 +1,163 @@
|
||||
//
|
||||
// Selector.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 21/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* CSS-like element selector, that finds elements matching a query.
|
||||
*
|
||||
* <h2>Selector syntax</h2>
|
||||
* <p>
|
||||
* A selector is a chain of simple selectors, separated by combinators. Selectors are <b>case insensitive</b> (including against
|
||||
* elements, attributes, and attribute values).
|
||||
* </p>
|
||||
* <p>
|
||||
* The universal selector (*) is implicit when no element selector is supplied (i.e. {@code *.header} and {@code .header}
|
||||
* is equivalent).
|
||||
* </p>
|
||||
* <table summary="">
|
||||
* <tr><th align="left">Pattern</th><th align="left">Matches</th><th align="left">Example</th></tr>
|
||||
* <tr><td><code>*</code></td><td>any element</td><td><code>*</code></td></tr>
|
||||
* <tr><td><code>tag</code></td><td>elements with the given tag name</td><td><code>div</code></td></tr>
|
||||
* <tr><td><code>*|E</code></td><td>elements of type E in any namespace <i>ns</i></td><td><code>*|name</code> finds <code><fb:name></code> elements</td></tr>
|
||||
* <tr><td><code>ns|E</code></td><td>elements of type E in the namespace <i>ns</i></td><td><code>fb|name</code> finds <code><fb:name></code> elements</td></tr>
|
||||
* <tr><td><code>#id</code></td><td>elements with attribute ID of "id"</td><td><code>div#wrap</code>, <code>#logo</code></td></tr>
|
||||
* <tr><td><code>.class</code></td><td>elements with a class name of "class"</td><td><code>div.left</code>, <code>.result</code></td></tr>
|
||||
* <tr><td><code>[attr]</code></td><td>elements with an attribute named "attr" (with any value)</td><td><code>a[href]</code>, <code>[title]</code></td></tr>
|
||||
* <tr><td><code>[^attrPrefix]</code></td><td>elements with an attribute name starting with "attrPrefix". Use to find elements with HTML5 datasets</td><td><code>[^data-]</code>, <code>div[^data-]</code></td></tr>
|
||||
* <tr><td><code>[attr=val]</code></td><td>elements with an attribute named "attr", and value equal to "val"</td><td><code>img[width=500]</code>, <code>a[rel=nofollow]</code></td></tr>
|
||||
* <tr><td><code>[attr="val"]</code></td><td>elements with an attribute named "attr", and value equal to "val"</td><td><code>span[hello="Cleveland"][goodbye="Columbus"]</code>, <code>a[rel="nofollow"]</code></td></tr>
|
||||
* <tr><td><code>[attr^=valPrefix]</code></td><td>elements with an attribute named "attr", and value starting with "valPrefix"</td><td><code>a[href^=http:]</code></td></tr>
|
||||
* <tr><td><code>[attr$=valSuffix]</code></td><td>elements with an attribute named "attr", and value ending with "valSuffix"</td><td><code>img[src$=.png]</code></td></tr>
|
||||
* <tr><td><code>[attr*=valContaining]</code></td><td>elements with an attribute named "attr", and value containing "valContaining"</td><td><code>a[href*=/search/]</code></td></tr>
|
||||
* <tr><td><code>[attr~=<em>regex</em>]</code></td><td>elements with an attribute named "attr", and value matching the regular expression</td><td><code>img[src~=(?i)\\.(png|jpe?g)]</code></td></tr>
|
||||
* <tr><td></td><td>The above may be combined in any order</td><td><code>div.header[title]</code></td></tr>
|
||||
* <tr><td><td colspan="3"><h3>Combinators</h3></td></tr>
|
||||
* <tr><td><code>E F</code></td><td>an F element descended from an E element</td><td><code>div a</code>, <code>.logo h1</code></td></tr>
|
||||
* <tr><td><code>E {@literal >} F</code></td><td>an F direct child of E</td><td><code>ol {@literal >} li</code></td></tr>
|
||||
* <tr><td><code>E + F</code></td><td>an F element immediately preceded by sibling E</td><td><code>li + li</code>, <code>div.head + div</code></td></tr>
|
||||
* <tr><td><code>E ~ F</code></td><td>an F element preceded by sibling E</td><td><code>h1 ~ p</code></td></tr>
|
||||
* <tr><td><code>E, F, G</code></td><td>all matching elements E, F, or G</td><td><code>a[href], div, h3</code></td></tr>
|
||||
* <tr><td><td colspan="3"><h3>Pseudo selectors</h3></td></tr>
|
||||
* <tr><td><code>:lt(<em>n</em>)</code></td><td>elements whose sibling index is less than <em>n</em></td><td><code>td:lt(3)</code> finds the first 3 cells of each row</td></tr>
|
||||
* <tr><td><code>:gt(<em>n</em>)</code></td><td>elements whose sibling index is greater than <em>n</em></td><td><code>td:gt(1)</code> finds cells after skipping the first two</td></tr>
|
||||
* <tr><td><code>:eq(<em>n</em>)</code></td><td>elements whose sibling index is equal to <em>n</em></td><td><code>td:eq(0)</code> finds the first cell of each row</td></tr>
|
||||
* <tr><td><code>:has(<em>selector</em>)</code></td><td>elements that contains at least one element matching the <em>selector</em></td><td><code>div:has(p)</code> finds divs that contain p elements </td></tr>
|
||||
* <tr><td><code>:not(<em>selector</em>)</code></td><td>elements that do not match the <em>selector</em>. See also {@link Elements#not(String)}</td><td><code>div:not(.logo)</code> finds all divs that do not have the "logo" class.<p><code>div:not(:has(div))</code> finds divs that do not contain divs.</p></td></tr>
|
||||
* <tr><td><code>:contains(<em>text</em>)</code></td><td>elements that contains the specified text. The search is case insensitive. The text may appear in the found element, or any of its descendants.</td><td><code>p:contains(jsoup)</code> finds p elements containing the text "jsoup".</td></tr>
|
||||
* <tr><td><code>:matches(<em>regex</em>)</code></td><td>elements whose text matches the specified regular expression. The text may appear in the found element, or any of its descendants.</td><td><code>td:matches(\\d+)</code> finds table cells containing digits. <code>div:matches((?i)login)</code> finds divs containing the text, case insensitively.</td></tr>
|
||||
* <tr><td><code>:containsOwn(<em>text</em>)</code></td><td>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.</td><td><code>p:containsOwn(jsoup)</code> finds p elements with own text "jsoup".</td></tr>
|
||||
* <tr><td><code>:matchesOwn(<em>regex</em>)</code></td><td>elements whose own text matches the specified regular expression. The text must appear in the found element, not any of its descendants.</td><td><code>td:matchesOwn(\\d+)</code> finds table cells directly containing digits. <code>div:matchesOwn((?i)login)</code> finds divs containing the text, case insensitively.</td></tr>
|
||||
* <tr><td></td><td>The above may be combined in any order and with other selectors</td><td><code>.light:contains(name):eq(0)</code></td></tr>
|
||||
* <tr><td colspan="3"><h3>Structural pseudo selectors</h3></td></tr>
|
||||
* <tr><td><code>:root</code></td><td>The element that is the root of the document. In HTML, this is the <code>html</code> element</td><td><code>:root</code></td></tr>
|
||||
* <tr><td><code>:nth-child(<em>a</em>n+<em>b</em>)</code></td><td><p>elements that have <code><em>a</em>n+<em>b</em>-1</code> siblings <b>before</b> it in the document tree, for any positive integer or zero value of <code>n</code>, and has a parent element. For values of <code>a</code> and <code>b</code> greater than zero, this effectively divides the element's children into groups of a elements (the last group taking the remainder), and selecting the <em>b</em>th element of each group. For example, this allows the selectors to address every other row in a table, and could be used to alternate the color of paragraph text in a cycle of four. The <code>a</code> and <code>b</code> values must be integers (positive, negative, or zero). The index of the first child of an element is 1.</p>
|
||||
* In addition to this, <code>:nth-child()</code> can take <code>odd</code> and <code>even</code> as arguments instead. <code>odd</code> has the same signification as <code>2n+1</code>, and <code>even</code> has the same signification as <code>2n</code>.</td><td><code>tr:nth-child(2n+1)</code> finds every odd row of a table. <code>:nth-child(10n-1)</code> the 9th, 19th, 29th, etc, element. <code>li:nth-child(5)</code> the 5h li</td></tr>
|
||||
* <tr><td><code>:nth-last-child(<em>a</em>n+<em>b</em>)</code></td><td>elements that have <code><em>a</em>n+<em>b</em>-1</code> siblings <b>after</b> it in the document tree. Otherwise like <code>:nth-child()</code></td><td><code>tr:nth-last-child(-n+2)</code> the last two rows of a table</td></tr>
|
||||
* <tr><td><code>:nth-of-type(<em>a</em>n+<em>b</em>)</code></td><td>pseudo-class notation represents an element that has <code><em>a</em>n+<em>b</em>-1</code> siblings with the same expanded element name <em>before</em> it in the document tree, for any zero or positive integer value of n, and has a parent element</td><td><code>img:nth-of-type(2n+1)</code></td></tr>
|
||||
* <tr><td><code>:nth-last-of-type(<em>a</em>n+<em>b</em>)</code></td><td>pseudo-class notation represents an element that has <code><em>a</em>n+<em>b</em>-1</code> siblings with the same expanded element name <em>after</em> it in the document tree, for any zero or positive integer value of n, and has a parent element</td><td><code>img:nth-last-of-type(2n+1)</code></td></tr>
|
||||
* <tr><td><code>:first-child</code></td><td>elements that are the first child of some other element.</td><td><code>div {@literal >} p:first-child</code></td></tr>
|
||||
* <tr><td><code>:last-child</code></td><td>elements that are the last child of some other element.</td><td><code>ol {@literal >} li:last-child</code></td></tr>
|
||||
* <tr><td><code>:first-of-type</code></td><td>elements that are the first sibling of its type in the list of children of its parent element</td><td><code>dl dt:first-of-type</code></td></tr>
|
||||
* <tr><td><code>:last-of-type</code></td><td>elements that are the last sibling of its type in the list of children of its parent element</td><td><code>tr {@literal >} td:last-of-type</code></td></tr>
|
||||
* <tr><td><code>:only-child</code></td><td>elements that have a parent element and whose parent element hasve no other element children</td><td></td></tr>
|
||||
* <tr><td><code>:only-of-type</code></td><td> an element that has a parent element and whose parent element has no other element children with the same expanded element name</td><td></td></tr>
|
||||
* <tr><td><code>:empty</code></td><td>elements that have no children at all</td><td></td></tr>
|
||||
* </table>
|
||||
*
|
||||
* @see Element#select(String)
|
||||
*/
|
||||
open class Selector {
|
||||
private let evaluator: Evaluator
|
||||
private let root: Element
|
||||
|
||||
private init(_ query: String, _ root: Element)throws {
|
||||
let query = query.trim()
|
||||
try Validate.notEmpty(string: query)
|
||||
|
||||
self.evaluator = try QueryParser.parse(query)
|
||||
|
||||
self.root = root
|
||||
}
|
||||
|
||||
private init(_ evaluator: Evaluator, _ root: Element) {
|
||||
self.evaluator = evaluator
|
||||
self.root = root
|
||||
}
|
||||
|
||||
/**
|
||||
* Find elements matching selector.
|
||||
*
|
||||
* @param query CSS selector
|
||||
* @param root root element to descend into
|
||||
* @return matching elements, empty if none
|
||||
* @throws Selector.SelectorParseException (unchecked) on an invalid CSS query.
|
||||
*/
|
||||
public static func select(_ query: String, _ root: Element)throws->Elements {
|
||||
return try Selector(query, root).select()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find elements matching selector.
|
||||
*
|
||||
* @param evaluator CSS selector
|
||||
* @param root root element to descend into
|
||||
* @return matching elements, empty if none
|
||||
*/
|
||||
public static func select(_ evaluator: Evaluator, _ root: Element)throws->Elements {
|
||||
return try Selector(evaluator, root).select()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find elements matching selector.
|
||||
*
|
||||
* @param query CSS selector
|
||||
* @param roots root elements to descend into
|
||||
* @return matching elements, empty if none
|
||||
*/
|
||||
public static func select(_ query: String, _ roots: Array<Element>)throws->Elements {
|
||||
try Validate.notEmpty(string: query)
|
||||
let evaluator: Evaluator = try QueryParser.parse(query)
|
||||
var elements: Array<Element> = Array<Element>()
|
||||
var seenElements: Array<Element> = Array<Element>()
|
||||
// dedupe elements by identity, not equality
|
||||
|
||||
for root: Element in roots {
|
||||
let found: Elements = try select(evaluator, root)
|
||||
for el: Element in found.array() {
|
||||
if (!seenElements.contains(el)) {
|
||||
elements.append(el)
|
||||
seenElements.append(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Elements(elements)
|
||||
}
|
||||
|
||||
private func select()throws->Elements {
|
||||
return try Collector.collect(evaluator, root)
|
||||
}
|
||||
|
||||
// exclude set. package open so that Elements can implement .not() selector.
|
||||
static func filterOut(_ elements: Array<Element>, _ outs: Array<Element>) -> Elements {
|
||||
let output: Elements = Elements()
|
||||
for el: Element in elements {
|
||||
var found: Bool = false
|
||||
for out: Element in outs {
|
||||
if (el.equals(out)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
output.add(el)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// SerializationException.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
//TODO:
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// SimpleDictionary.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 30/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class SimpleDictionary<KeyType: Hashable, ValueType> {
|
||||
|
||||
public typealias DictionaryType = [KeyType: ValueType]
|
||||
public private(set) var values = DictionaryType()
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
return values.count
|
||||
}
|
||||
|
||||
public func remove(_ key: KeyType) {
|
||||
values.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
public func contains(_ key: KeyType) -> Bool {
|
||||
return self.values[key] != nil
|
||||
}
|
||||
|
||||
public func put(_ value: ValueType, forKey key: KeyType) {
|
||||
self.values[key] = value
|
||||
}
|
||||
|
||||
public func get(_ key: KeyType) -> ValueType? {
|
||||
return self.values[key]
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// StreamReader.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 08/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class StreamReader {
|
||||
|
||||
let encoding: String.Encoding
|
||||
let chunkSize: Int
|
||||
var fileHandle: FileHandle!
|
||||
let delimData: Data
|
||||
var buffer: Data
|
||||
var atEof: Bool
|
||||
|
||||
init?(path: String, delimiter: String = "\n", encoding: String.Encoding = .utf8,
|
||||
chunkSize: Int = 4096) {
|
||||
|
||||
guard let fileHandle = FileHandle(forReadingAtPath: path),
|
||||
let delimData = delimiter.data(using: encoding) else {
|
||||
return nil
|
||||
}
|
||||
self.encoding = encoding
|
||||
self.chunkSize = chunkSize
|
||||
self.fileHandle = fileHandle
|
||||
self.delimData = delimData
|
||||
self.buffer = Data(capacity: chunkSize)
|
||||
self.atEof = false
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.close()
|
||||
}
|
||||
|
||||
/// Return next line, or nil on EOF.
|
||||
func nextLine() -> String? {
|
||||
precondition(fileHandle != nil, "Attempt to read from closed file")
|
||||
|
||||
// Read data chunks from file until a line delimiter is found:
|
||||
while !atEof {
|
||||
if let range = buffer.range(of: delimData) {
|
||||
// Convert complete line (excluding the delimiter) to a string:
|
||||
let line = String(data: buffer.subdata(in: 0..<range.lowerBound), encoding: encoding)
|
||||
// Remove line (and the delimiter) from the buffer:
|
||||
buffer.removeSubrange(0..<range.upperBound)
|
||||
return line
|
||||
}
|
||||
let tmpData = fileHandle.readData(ofLength: chunkSize)
|
||||
if tmpData.count > 0 {
|
||||
buffer.append(tmpData)
|
||||
} else {
|
||||
// EOF or read error.
|
||||
atEof = true
|
||||
if buffer.count > 0 {
|
||||
// Buffer contains last line in file (not terminated by delimiter).
|
||||
let line = String(data: buffer as Data, encoding: encoding)
|
||||
buffer.count = 0
|
||||
return line
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Start reading from the beginning of file.
|
||||
func rewind() {
|
||||
fileHandle.seek(toFileOffset: 0)
|
||||
buffer.count = 0
|
||||
atEof = false
|
||||
}
|
||||
|
||||
/// Close the underlying file. No reading must be done after calling this method.
|
||||
func close() {
|
||||
fileHandle?.closeFile()
|
||||
fileHandle = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension StreamReader: Sequence {
|
||||
func makeIterator() -> AnyIterator<String> {
|
||||
return AnyIterator {
|
||||
return self.nextLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+206
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// String.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 21/04/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
subscript (i: Int) -> Character {
|
||||
return self[self.index(self.startIndex, offsetBy: i)]
|
||||
}
|
||||
|
||||
subscript (i: Int) -> String {
|
||||
return String(self[i] as Character)
|
||||
}
|
||||
|
||||
init<S: Sequence>(_ ucs: S)where S.Iterator.Element == UnicodeScalar {
|
||||
var s = ""
|
||||
s.unicodeScalars.append(contentsOf: ucs)
|
||||
self = s
|
||||
}
|
||||
|
||||
func unicodeScalar(_ i: Int) -> UnicodeScalar {
|
||||
return self.unicodeScalars.prefix(i+1).last!
|
||||
}
|
||||
|
||||
func string(_ offset: Int, _ count: Int) -> String {
|
||||
let truncStart = self.unicodeScalars.count-offset
|
||||
return String(self.unicodeScalars.suffix(truncStart).prefix(count))
|
||||
}
|
||||
|
||||
static func split(_ value: String, _ offset: Int, _ count: Int) -> String {
|
||||
let start = value.index(value.startIndex, offsetBy: offset)
|
||||
let end = value.index(value.startIndex, offsetBy: count+offset)
|
||||
#if swift(>=4)
|
||||
return String(value[start..<end])
|
||||
#else
|
||||
let range = start..<end
|
||||
return value.substring(with: range)
|
||||
#endif
|
||||
}
|
||||
|
||||
func isEmptyOrWhitespace() -> Bool {
|
||||
|
||||
if(self.isEmpty) {
|
||||
return true
|
||||
}
|
||||
return (self.trimmingCharacters(in: CharacterSet.whitespaces) == "")
|
||||
}
|
||||
|
||||
func startsWith(_ string: String) -> Bool {
|
||||
return self.hasPrefix(string)
|
||||
}
|
||||
|
||||
func indexOf(_ substring: String, _ offset: Int ) -> Int {
|
||||
if(offset > count) {return -1}
|
||||
|
||||
let maxIndex = self.count - substring.count
|
||||
if(maxIndex >= 0) {
|
||||
for index in offset...maxIndex {
|
||||
let rangeSubstring = self.index(self.startIndex, offsetBy: index)..<self.index(self.startIndex, offsetBy: index + substring.count)
|
||||
#if swift(>=4)
|
||||
let selfSubstring = self[rangeSubstring]
|
||||
#else
|
||||
let selfSubstring = self.substring(with: rangeSubstring)
|
||||
#endif
|
||||
if selfSubstring == substring {
|
||||
return index
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func indexOf(_ substring: String) -> Int {
|
||||
return self.indexOf(substring, 0)
|
||||
}
|
||||
|
||||
func trim() -> String {
|
||||
return trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
func equalsIgnoreCase(string: String?) -> Bool {
|
||||
if(string == nil) {return false}
|
||||
return string!.lowercased() == lowercased()
|
||||
}
|
||||
|
||||
static func toHexString(n: Int) -> String {
|
||||
return String(format: "%2x", n)
|
||||
}
|
||||
|
||||
func insert(string: String, ind: Int) -> String {
|
||||
return String(self.prefix(ind)) + string + String(self.suffix(self.count-ind))
|
||||
}
|
||||
|
||||
func charAt(_ i: Int) -> Character {
|
||||
return self[i] as Character
|
||||
}
|
||||
|
||||
func substring(_ beginIndex: Int) -> String {
|
||||
return String.split(self, beginIndex, self.count-beginIndex)
|
||||
}
|
||||
|
||||
func substring(_ beginIndex: Int, _ count: Int) -> String {
|
||||
return String.split(self, beginIndex, count)
|
||||
}
|
||||
|
||||
func regionMatches(_ ignoreCase: Bool, _ selfOffset: Int, _ other: String, _ otherOffset: Int, _ length: Int ) -> Bool {
|
||||
if ((otherOffset < 0) || (selfOffset < 0)
|
||||
|| (selfOffset > self.count - length)
|
||||
|| (otherOffset > other.count - length)) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i in 0..<length {
|
||||
let charSelf: Character = self[i+selfOffset]
|
||||
let charOther: Character = other[i+otherOffset]
|
||||
if(ignoreCase) {
|
||||
if(charSelf.lowercase != charOther.lowercase) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if(charSelf != charOther) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func startsWith(_ input: String, _ offset: Int) -> Bool {
|
||||
if ((offset < 0) || (offset > count - input.count)) {
|
||||
return false
|
||||
}
|
||||
for i in 0..<input.count {
|
||||
let charSelf: Character = self[i+offset]
|
||||
let charOther: Character = input[i]
|
||||
if(charSelf != charOther) {return false}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func replaceFirst(of pattern: String, with replacement: String) -> String {
|
||||
if let range = self.range(of: pattern) {
|
||||
return self.replacingCharacters(in: range, with: replacement)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
func replaceAll(of pattern: String, with replacement: String, options: NSRegularExpression.Options = []) -> String {
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(0..<self.utf16.count)
|
||||
return regex.stringByReplacingMatches(in: self, options: [],
|
||||
range: range, withTemplate: replacement)
|
||||
} catch {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
func equals(_ s: String?) -> Bool {
|
||||
if(s == nil) {return false}
|
||||
return self == s!
|
||||
}
|
||||
}
|
||||
|
||||
extension String.Encoding {
|
||||
func canEncode(_ string: String) -> Bool {
|
||||
return string.cString(using: self) != nil
|
||||
}
|
||||
|
||||
public func displayName() -> String {
|
||||
switch self {
|
||||
case String.Encoding.ascii: return "US-ASCII"
|
||||
case String.Encoding.nextstep: return "nextstep"
|
||||
case String.Encoding.japaneseEUC: return "EUC-JP"
|
||||
case String.Encoding.utf8: return "UTF-8"
|
||||
case String.Encoding.isoLatin1: return "csISOLatin1"
|
||||
case String.Encoding.symbol: return "MacSymbol"
|
||||
case String.Encoding.nonLossyASCII: return "nonLossyASCII"
|
||||
case String.Encoding.shiftJIS: return "shiftJIS"
|
||||
case String.Encoding.isoLatin2: return "csISOLatin2"
|
||||
case String.Encoding.unicode: return "unicode"
|
||||
case String.Encoding.windowsCP1251: return "windows-1251"
|
||||
case String.Encoding.windowsCP1252: return "windows-1252"
|
||||
case String.Encoding.windowsCP1253: return "windows-1253"
|
||||
case String.Encoding.windowsCP1254: return "windows-1254"
|
||||
case String.Encoding.windowsCP1250: return "windows-1250"
|
||||
case String.Encoding.iso2022JP: return "iso2022jp"
|
||||
case String.Encoding.macOSRoman: return "macOSRoman"
|
||||
case String.Encoding.utf16: return "UTF-16"
|
||||
case String.Encoding.utf16BigEndian: return "UTF-16BE"
|
||||
case String.Encoding.utf16LittleEndian: return "UTF-16LE"
|
||||
case String.Encoding.utf32: return "UTF-32"
|
||||
case String.Encoding.utf32BigEndian: return "UTF-32BE"
|
||||
case String.Encoding.utf32LittleEndian: return "UTF-32LE"
|
||||
default:
|
||||
return self.description
|
||||
}
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
Supports creation of a String from pieces
|
||||
https://gist.github.com/kristopherjohnson/1fc55e811d944a430289
|
||||
*/
|
||||
open class StringBuilder {
|
||||
fileprivate var stringValue: Array<Character>
|
||||
|
||||
/**
|
||||
Construct with initial String contents
|
||||
|
||||
:param: string Initial value; defaults to empty string
|
||||
*/
|
||||
public init(string: String = "") {
|
||||
self.stringValue = Array(string)
|
||||
}
|
||||
|
||||
public init(_ size: Int) {
|
||||
self.stringValue = Array()
|
||||
}
|
||||
|
||||
/**
|
||||
Return the String object
|
||||
|
||||
:return: String
|
||||
*/
|
||||
open func toString() -> String {
|
||||
return String(stringValue)
|
||||
}
|
||||
|
||||
/**
|
||||
Return the current length of the String object
|
||||
*/
|
||||
open var length: Int {
|
||||
return self.stringValue.count
|
||||
//return countElements(stringValue)
|
||||
}
|
||||
|
||||
/**
|
||||
Append a String to the object
|
||||
|
||||
:param: string String
|
||||
|
||||
:return: reference to this StringBuilder instance
|
||||
*/
|
||||
open func append(_ string: String) {
|
||||
stringValue.append(contentsOf: string)
|
||||
}
|
||||
|
||||
open func appendCodePoint(_ chr: Character) {
|
||||
stringValue.append(chr)
|
||||
}
|
||||
|
||||
open func appendCodePoints(_ chr: [Character]) {
|
||||
stringValue.append(contentsOf: chr)
|
||||
}
|
||||
|
||||
open func appendCodePoint(_ ch: Int) {
|
||||
stringValue.append(Character(UnicodeScalar(ch)!))
|
||||
}
|
||||
|
||||
open func appendCodePoint(_ ch: UnicodeScalar) {
|
||||
stringValue.append(Character(ch))
|
||||
}
|
||||
|
||||
open func appendCodePoints(_ chr: [UnicodeScalar]) {
|
||||
for c in chr {
|
||||
appendCodePoint(c)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Append a Printable to the object
|
||||
|
||||
:param: value a value supporting the Printable protocol
|
||||
|
||||
:return: reference to this StringBuilder instance
|
||||
*/
|
||||
@discardableResult
|
||||
open func append<T: CustomStringConvertible>(_ value: T) -> StringBuilder {
|
||||
stringValue.append(contentsOf: value.description)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func append(_ value: UnicodeScalar) -> StringBuilder {
|
||||
stringValue.append(contentsOf: value.description)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func insert<T: CustomStringConvertible>(_ offset: Int, _ value: T) -> StringBuilder {
|
||||
stringValue.insert(contentsOf: value.description, at: offset)
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Append a String and a newline to the object
|
||||
|
||||
:param: string String
|
||||
|
||||
:return: reference to this StringBuilder instance
|
||||
*/
|
||||
@discardableResult
|
||||
open func appendLine(_ string: String) -> StringBuilder {
|
||||
stringValue.append(contentsOf: "\n")
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Append a Printable and a newline to the object
|
||||
|
||||
:param: value a value supporting the Printable protocol
|
||||
|
||||
:return: reference to this StringBuilder instance
|
||||
*/
|
||||
@discardableResult
|
||||
open func appendLine<T: CustomStringConvertible>(_ value: T) -> StringBuilder {
|
||||
stringValue.append(contentsOf: value.description)
|
||||
stringValue.append(contentsOf: "\n")
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Reset the object to an empty string
|
||||
|
||||
:return: reference to this StringBuilder instance
|
||||
*/
|
||||
@discardableResult
|
||||
open func clear() -> StringBuilder {
|
||||
stringValue = Array()
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Append a String to a StringBuilder using operator syntax
|
||||
|
||||
:param: lhs StringBuilder
|
||||
:param: rhs String
|
||||
*/
|
||||
public func += (lhs: StringBuilder, rhs: String) {
|
||||
lhs.append(rhs)
|
||||
}
|
||||
|
||||
/**
|
||||
Append a Printable to a StringBuilder using operator syntax
|
||||
|
||||
:param: lhs Printable
|
||||
:param: rhs String
|
||||
*/
|
||||
public func += <T: CustomStringConvertible>(lhs: StringBuilder, rhs: T) {
|
||||
lhs.append(rhs.description)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a StringBuilder by concatenating the values of two StringBuilders
|
||||
|
||||
:param: lhs first StringBuilder
|
||||
:param: rhs second StringBuilder
|
||||
|
||||
:result StringBuilder
|
||||
*/
|
||||
public func +(lhs: StringBuilder, rhs: StringBuilder) -> StringBuilder {
|
||||
return StringBuilder(string: lhs.toString() + rhs.toString())
|
||||
}
|
||||
Generated
+265
@@ -0,0 +1,265 @@
|
||||
//
|
||||
// StringUtil.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 20/04/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* A minimal String utility class. Designed for internal jsoup use only.
|
||||
*/
|
||||
open class StringUtil {
|
||||
enum StringError: Error {
|
||||
case empty
|
||||
case short
|
||||
case error(String)
|
||||
}
|
||||
|
||||
// memoised padding up to 10
|
||||
fileprivate static var padding: [String] = ["", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]
|
||||
|
||||
/**
|
||||
* Join a collection of strings by a seperator
|
||||
* @param strings collection of string objects
|
||||
* @param sep string to place between strings
|
||||
* @return joined string
|
||||
*/
|
||||
public static func join(_ strings: [String], sep: String) -> String {
|
||||
return strings.joined(separator: sep)
|
||||
}
|
||||
public static func join(_ strings: Set<String>, sep: String) -> String {
|
||||
return strings.joined(separator: sep)
|
||||
}
|
||||
|
||||
public static func join(_ strings: OrderedSet<String>, sep: String) -> String {
|
||||
return strings.joined(separator: sep)
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Join a collection of strings by a seperator
|
||||
// * @param strings iterator of string objects
|
||||
// * @param sep string to place between strings
|
||||
// * @return joined string
|
||||
// */
|
||||
// public static String join(Iterator strings, String sep) {
|
||||
// if (!strings.hasNext())
|
||||
// return ""
|
||||
//
|
||||
// String start = strings.next().toString()
|
||||
// if (!strings.hasNext()) // only one, avoid builder
|
||||
// return start
|
||||
//
|
||||
// StringBuilder sb = new StringBuilder(64).append(start)
|
||||
// while (strings.hasNext()) {
|
||||
// sb.append(sep)
|
||||
// sb.append(strings.next())
|
||||
// }
|
||||
// return sb.toString()
|
||||
// }
|
||||
/**
|
||||
* Returns space padding
|
||||
* @param width amount of padding desired
|
||||
* @return string of spaces * width
|
||||
*/
|
||||
public static func padding(_ width: Int) -> String {
|
||||
|
||||
if(width <= 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (width < padding.count) {
|
||||
return padding[width]
|
||||
}
|
||||
|
||||
var out: [Character] = [Character]()
|
||||
|
||||
for _ in 0..<width {
|
||||
out.append(" ")
|
||||
}
|
||||
return String(out)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a string is blank: null, emtpy, or only whitespace (" ", \r\n, \t, etc)
|
||||
* @param string string to test
|
||||
* @return if string is blank
|
||||
*/
|
||||
public static func isBlank(_ string: String) -> Bool {
|
||||
if (string.count == 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
for chr in string {
|
||||
if (!StringUtil.isWhitespace(chr)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a string is numeric, i.e. contains only digit characters
|
||||
* @param string string to test
|
||||
* @return true if only digit chars, false if empty or null or contains non-digit chrs
|
||||
*/
|
||||
public static func isNumeric(_ string: String) -> Bool {
|
||||
if (string.count == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for chr in string {
|
||||
if !("0"..."9" ~= chr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a code point is "whitespace" as defined in the HTML spec.
|
||||
* @param c code point to test
|
||||
* @return true if code point is whitespace, false otherwise
|
||||
*/
|
||||
public static func isWhitespace(_ c: Character) -> Bool {
|
||||
//(c == " " || c == UnicodeScalar.BackslashT || c == "\n" || (c == "\f" ) || c == "\r")
|
||||
return c.isWhitespace
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the whitespace within this string; multiple spaces collapse to a single, and all whitespace characters
|
||||
* (e.g. newline, tab) convert to a simple space
|
||||
* @param string content to normalise
|
||||
* @return normalised string
|
||||
*/
|
||||
public static func normaliseWhitespace(_ string: String) -> String {
|
||||
let sb: StringBuilder = StringBuilder.init()
|
||||
appendNormalisedWhitespace(sb, string: string, stripLeading: false)
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* After normalizing the whitespace within a string, appends it to a string builder.
|
||||
* @param accum builder to append to
|
||||
* @param string string to normalize whitespace within
|
||||
* @param stripLeading set to true if you wish to remove any leading whitespace
|
||||
*/
|
||||
public static func appendNormalisedWhitespace(_ accum: StringBuilder, string: String, stripLeading: Bool ) {
|
||||
var lastWasWhite: Bool = false
|
||||
var reachedNonWhite: Bool = false
|
||||
|
||||
for c in string {
|
||||
if (isWhitespace(c)) {
|
||||
if ((stripLeading && !reachedNonWhite) || lastWasWhite) {
|
||||
continue
|
||||
}
|
||||
accum.append(" ")
|
||||
lastWasWhite = true
|
||||
} else {
|
||||
accum.appendCodePoint(c)
|
||||
lastWasWhite = false
|
||||
reachedNonWhite = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func inString(_ needle: String?, haystack: String...) -> Bool {
|
||||
return inString(needle, haystack)
|
||||
}
|
||||
public static func inString(_ needle: String?, _ haystack: [String?]) -> Bool {
|
||||
if(needle == nil) {return false}
|
||||
for hay in haystack {
|
||||
if(hay != nil && hay! == needle!) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// open static func inSorted(_ needle: String, haystack: [String]) -> Bool {
|
||||
// return binarySearch(haystack, searchItem: needle) >= 0
|
||||
// }
|
||||
//
|
||||
// open static func binarySearch<T: Comparable>(_ inputArr: Array<T>, searchItem: T) -> Int {
|
||||
// var lowerIndex = 0
|
||||
// var upperIndex = inputArr.count - 1
|
||||
//
|
||||
// while (true) {
|
||||
// let currentIndex = (lowerIndex + upperIndex)/2
|
||||
// if(inputArr[currentIndex] == searchItem) {
|
||||
// return currentIndex
|
||||
// } else if (lowerIndex > upperIndex) {
|
||||
// return -1
|
||||
// } else {
|
||||
// if (inputArr[currentIndex] > searchItem) {
|
||||
// upperIndex = currentIndex - 1
|
||||
// } else {
|
||||
// lowerIndex = currentIndex + 1
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Create a new absolute URL, from a provided existing absolute URL and a relative URL component.
|
||||
* @param base the existing absolulte base URL
|
||||
* @param relUrl the relative URL to resolve. (If it's already absolute, it will be returned)
|
||||
* @return the resolved absolute URL
|
||||
* @throws MalformedURLException if an error occurred generating the URL
|
||||
*/
|
||||
//NOTE: Not sure it work
|
||||
public static func resolve(_ base: URL, relUrl: String ) -> URL? {
|
||||
var base = base
|
||||
if(base.pathComponents.count == 0 && base.absoluteString.last != "/" && !base.isFileURL) {
|
||||
base = base.appendingPathComponent("/", isDirectory: false)
|
||||
}
|
||||
let u = URL(string: relUrl, relativeTo: base)
|
||||
return u
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new absolute URL, from a provided existing absolute URL and a relative URL component.
|
||||
* @param baseUrl the existing absolute base URL
|
||||
* @param relUrl the relative URL to resolve. (If it's already absolute, it will be returned)
|
||||
* @return an absolute URL if one was able to be generated, or the empty string if not
|
||||
*/
|
||||
public static func resolve(_ baseUrl: String, relUrl: String ) -> String {
|
||||
|
||||
let base = URL(string: baseUrl)
|
||||
|
||||
if(base == nil || base?.scheme == nil) {
|
||||
let abs = URL(string: relUrl)
|
||||
return abs != nil && abs?.scheme != nil ? abs!.absoluteURL.absoluteString : ""
|
||||
} else {
|
||||
let url = resolve(base!, relUrl: relUrl)
|
||||
if(url != nil) {
|
||||
let ext = url!.absoluteURL.absoluteString
|
||||
return ext
|
||||
}
|
||||
|
||||
if(base != nil && base?.scheme != nil) {
|
||||
let ext = base!.absoluteString
|
||||
return ext
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// try {
|
||||
// try {
|
||||
// base = new URL(baseUrl)
|
||||
// } catch (MalformedURLException e) {
|
||||
// // the base is unsuitable, but the attribute/rel may be abs on its own, so try that
|
||||
// URL abs = new URL(relUrl)
|
||||
// return abs.toExternalForm()
|
||||
// }
|
||||
// return resolve(base, relUrl).toExternalForm()
|
||||
// } catch (MalformedURLException e) {
|
||||
// return ""
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// StructuralEvaluator.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 23/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Base structural evaluator.
|
||||
*/
|
||||
public class StructuralEvaluator: Evaluator {
|
||||
let evaluator: Evaluator
|
||||
|
||||
public init(_ evaluator: Evaluator) {
|
||||
self.evaluator = evaluator
|
||||
}
|
||||
|
||||
public class Root: Evaluator {
|
||||
public override func matches(_ root: Element, _ element: Element) -> Bool {
|
||||
return root === element
|
||||
}
|
||||
}
|
||||
|
||||
public class Has: StructuralEvaluator {
|
||||
public override init(_ evaluator: Evaluator) {
|
||||
super.init(evaluator)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
for e in try element.getAllElements().array() {
|
||||
do {
|
||||
if(e != element) {
|
||||
if ((try evaluator.matches(root, e))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":has(\(evaluator.toString()))"
|
||||
}
|
||||
}
|
||||
|
||||
public class Not: StructuralEvaluator {
|
||||
public override init(_ evaluator: Evaluator) {
|
||||
super.init(evaluator)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ node: Element) -> Bool {
|
||||
do {
|
||||
return try !evaluator.matches(root, node)
|
||||
} catch {}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":not\(evaluator.toString())"
|
||||
}
|
||||
}
|
||||
|
||||
public class Parent: StructuralEvaluator {
|
||||
public override init(_ evaluator: Evaluator) {
|
||||
super.init(evaluator)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element) -> Bool {
|
||||
if (root == element) {
|
||||
return false
|
||||
}
|
||||
|
||||
var parent = element.parent()
|
||||
while (true) {
|
||||
do {
|
||||
if parent != nil {
|
||||
if (try evaluator.matches(root, parent!)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (parent == root) {
|
||||
break
|
||||
}
|
||||
parent = parent?.parent()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":parent\(evaluator.toString())"
|
||||
}
|
||||
}
|
||||
|
||||
public class ImmediateParent: StructuralEvaluator {
|
||||
public override init(_ evaluator: Evaluator) {
|
||||
super.init(evaluator)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element) -> Bool {
|
||||
if (root == element) {
|
||||
return false
|
||||
}
|
||||
|
||||
if let parent = element.parent() {
|
||||
do {
|
||||
return try evaluator.matches(root, parent)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":ImmediateParent\(evaluator.toString())"
|
||||
}
|
||||
}
|
||||
|
||||
public class PreviousSibling: StructuralEvaluator {
|
||||
public override init(_ evaluator: Evaluator) {
|
||||
super.init(evaluator)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
if (root == element) {
|
||||
return false
|
||||
}
|
||||
|
||||
var prev = try element.previousElementSibling()
|
||||
|
||||
while (prev != nil) {
|
||||
do {
|
||||
if (try evaluator.matches(root, prev!)) {
|
||||
return true
|
||||
}
|
||||
} catch {}
|
||||
|
||||
prev = try prev!.previousElementSibling()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":prev*\(evaluator.toString())"
|
||||
}
|
||||
}
|
||||
|
||||
class ImmediatePreviousSibling: StructuralEvaluator {
|
||||
public override init(_ evaluator: Evaluator) {
|
||||
super.init(evaluator)
|
||||
}
|
||||
|
||||
public override func matches(_ root: Element, _ element: Element)throws->Bool {
|
||||
if (root == element) {
|
||||
return false
|
||||
}
|
||||
|
||||
if let prev = try element.previousElementSibling() {
|
||||
do {
|
||||
return try evaluator.matches(root, prev)
|
||||
} catch {}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public override func toString() -> String {
|
||||
return ":prev\(evaluator.toString())"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+242
@@ -0,0 +1,242 @@
|
||||
//
|
||||
// SwiftSoup.swift
|
||||
// Jsoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Parse HTML into a Document. The parser will make a sensible, balanced document tree out of any HTML.
|
||||
|
||||
@param html HTML to parse
|
||||
@param baseUri The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs, that occur
|
||||
before the HTML declares a {@code <base href>} tag.
|
||||
@return sane HTML
|
||||
*/
|
||||
public func parse(_ html: String, _ baseUri: String)throws->Document {
|
||||
return try Parser.parse(html, baseUri)
|
||||
}
|
||||
|
||||
/**
|
||||
Parse HTML into a Document, using the provided Parser. You can provide an alternate parser, such as a simple XML
|
||||
(non-HTML) parser.
|
||||
|
||||
@param html HTML to parse
|
||||
@param baseUri The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs, that occur
|
||||
before the HTML declares a {@code <base href>} tag.
|
||||
@param parser alternate {@link Parser#xmlParser() parser} to use.
|
||||
@return sane HTML
|
||||
*/
|
||||
public func parse(_ html: String, _ baseUri: String, _ parser: Parser)throws->Document {
|
||||
return try parser.parseInput(html, baseUri)
|
||||
}
|
||||
|
||||
/**
|
||||
Parse HTML into a Document. As no base URI is specified, absolute URL detection relies on the HTML including a
|
||||
{@code <base href>} tag.
|
||||
|
||||
@param html HTML to parse
|
||||
@return sane HTML
|
||||
|
||||
@see #parse(String, String)
|
||||
*/
|
||||
public func parse(_ html: String)throws->Document {
|
||||
return try Parser.parse(html, "")
|
||||
}
|
||||
|
||||
//todo:
|
||||
// /**
|
||||
// * Creates a new {@link Connection} to a URL. Use to fetch and parse a HTML page.
|
||||
// * <p>
|
||||
// * Use examples:
|
||||
// * <ul>
|
||||
// * <li><code>Document doc = Jsoup.connect("http://example.com").userAgent("Mozilla").data("name", "jsoup").get();</code></li>
|
||||
// * <li><code>Document doc = Jsoup.connect("http://example.com").cookie("auth", "token").post();</code></li>
|
||||
// * </ul>
|
||||
// * @param url URL to connect to. The protocol must be {@code http} or {@code https}.
|
||||
// * @return the connection. You can add data, cookies, and headers; set the user-agent, referrer, method; and then execute.
|
||||
// */
|
||||
// public static Connection connect(String url) {
|
||||
// return HttpConnection.connect(url);
|
||||
// }
|
||||
|
||||
//todo:
|
||||
// /**
|
||||
// Parse the contents of a file as HTML.
|
||||
//
|
||||
// @param in file to load HTML from
|
||||
// @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
|
||||
// present, or fall back to {@code UTF-8} (which is often safe to do).
|
||||
// @param baseUri The URL where the HTML was retrieved from, to resolve relative links against.
|
||||
// @return sane HTML
|
||||
//
|
||||
// @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
|
||||
// */
|
||||
// public static Document parse(File in, String charsetName, String baseUri) throws IOException {
|
||||
// return DataUtil.load(in, charsetName, baseUri);
|
||||
// }
|
||||
|
||||
//todo:
|
||||
// /**
|
||||
// Parse the contents of a file as HTML. The location of the file is used as the base URI to qualify relative URLs.
|
||||
//
|
||||
// @param in file to load HTML from
|
||||
// @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
|
||||
// present, or fall back to {@code UTF-8} (which is often safe to do).
|
||||
// @return sane HTML
|
||||
//
|
||||
// @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
|
||||
// @see #parse(File, String, String)
|
||||
// */
|
||||
// public static Document parse(File in, String charsetName) throws IOException {
|
||||
// return DataUtil.load(in, charsetName, in.getAbsolutePath());
|
||||
// }
|
||||
|
||||
// /**
|
||||
// Read an input stream, and parse it to a Document.
|
||||
//
|
||||
// @param in input stream to read. Make sure to close it after parsing.
|
||||
// @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
|
||||
// present, or fall back to {@code UTF-8} (which is often safe to do).
|
||||
// @param baseUri The URL where the HTML was retrieved from, to resolve relative links against.
|
||||
// @return sane HTML
|
||||
//
|
||||
// @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
|
||||
// */
|
||||
// public static Document parse(InputStream in, String charsetName, String baseUri) throws IOException {
|
||||
// return DataUtil.load(in, charsetName, baseUri);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// Read an input stream, and parse it to a Document. You can provide an alternate parser, such as a simple XML
|
||||
// (non-HTML) parser.
|
||||
//
|
||||
// @param in input stream to read. Make sure to close it after parsing.
|
||||
// @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
|
||||
// present, or fall back to {@code UTF-8} (which is often safe to do).
|
||||
// @param baseUri The URL where the HTML was retrieved from, to resolve relative links against.
|
||||
// @param parser alternate {@link Parser#xmlParser() parser} to use.
|
||||
// @return sane HTML
|
||||
//
|
||||
// @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
|
||||
// */
|
||||
// public static Document parse(InputStream in, String charsetName, String baseUri, Parser parser) throws IOException {
|
||||
// return DataUtil.load(in, charsetName, baseUri, parser);
|
||||
// }
|
||||
|
||||
/**
|
||||
Parse a fragment of HTML, with the assumption that it forms the {@code body} of the HTML.
|
||||
|
||||
@param bodyHtml body HTML fragment
|
||||
@param baseUri URL to resolve relative URLs against.
|
||||
@return sane HTML document
|
||||
|
||||
@see Document#body()
|
||||
*/
|
||||
public func parseBodyFragment(_ bodyHtml: String, _ baseUri: String)throws->Document {
|
||||
return try Parser.parseBodyFragment(bodyHtml, baseUri)
|
||||
}
|
||||
|
||||
/**
|
||||
Parse a fragment of HTML, with the assumption that it forms the {@code body} of the HTML.
|
||||
|
||||
@param bodyHtml body HTML fragment
|
||||
@return sane HTML document
|
||||
|
||||
@see Document#body()
|
||||
*/
|
||||
public func parseBodyFragment(_ bodyHtml: String)throws->Document {
|
||||
return try Parser.parseBodyFragment(bodyHtml, "")
|
||||
}
|
||||
|
||||
// /**
|
||||
// Fetch a URL, and parse it as HTML. Provided for compatibility; in most cases use {@link #connect(String)} instead.
|
||||
// <p>
|
||||
// The encoding character set is determined by the content-type header or http-equiv meta tag, or falls back to {@code UTF-8}.
|
||||
//
|
||||
// @param url URL to fetch (with a GET). The protocol must be {@code http} or {@code https}.
|
||||
// @param timeoutMillis Connection and read timeout, in milliseconds. If exceeded, IOException is thrown.
|
||||
// @return The parsed HTML.
|
||||
//
|
||||
// @throws java.net.MalformedURLException if the request URL is not a HTTP or HTTPS URL, or is otherwise malformed
|
||||
// @throws HttpStatusException if the response is not OK and HTTP response errors are not ignored
|
||||
// @throws UnsupportedMimeTypeException if the response mime type is not supported and those errors are not ignored
|
||||
// @throws java.net.SocketTimeoutException if the connection times out
|
||||
// @throws IOException if a connection or read error occurs
|
||||
//
|
||||
// @see #connect(String)
|
||||
// */
|
||||
// public static func parse(_ url: URL, _ timeoutMillis: Int)throws->Document {
|
||||
// Connection con = HttpConnection.connect(url);
|
||||
// con.timeout(timeoutMillis);
|
||||
// return con.get();
|
||||
// }
|
||||
|
||||
/**
|
||||
Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted
|
||||
tags and attributes.
|
||||
|
||||
@param bodyHtml input untrusted HTML (body fragment)
|
||||
@param baseUri URL to resolve relative URLs against
|
||||
@param whitelist white-list of permitted HTML elements
|
||||
@return safe HTML (body fragment)
|
||||
|
||||
@see Cleaner#clean(Document)
|
||||
*/
|
||||
public func clean(_ bodyHtml: String, _ baseUri: String, _ whitelist: Whitelist)throws->String? {
|
||||
let dirty: Document = try parseBodyFragment(bodyHtml, baseUri)
|
||||
let cleaner: Cleaner = Cleaner(whitelist)
|
||||
let clean: Document = try cleaner.clean(dirty)
|
||||
return try clean.body()?.html()
|
||||
}
|
||||
|
||||
/**
|
||||
Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted
|
||||
tags and attributes.
|
||||
|
||||
@param bodyHtml input untrusted HTML (body fragment)
|
||||
@param whitelist white-list of permitted HTML elements
|
||||
@return safe HTML (body fragment)
|
||||
|
||||
@see Cleaner#clean(Document)
|
||||
*/
|
||||
public func clean(_ bodyHtml: String, _ whitelist: Whitelist)throws->String? {
|
||||
return try SwiftSoup.clean(bodyHtml, "", whitelist)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of
|
||||
* permitted
|
||||
* tags and attributes.
|
||||
*
|
||||
* @param bodyHtml input untrusted HTML (body fragment)
|
||||
* @param baseUri URL to resolve relative URLs against
|
||||
* @param whitelist white-list of permitted HTML elements
|
||||
* @param outputSettings document output settings; use to control pretty-printing and entity escape modes
|
||||
* @return safe HTML (body fragment)
|
||||
* @see Cleaner#clean(Document)
|
||||
*/
|
||||
public func clean(_ bodyHtml: String, _ baseUri: String, _ whitelist: Whitelist, _ outputSettings: OutputSettings)throws->String? {
|
||||
let dirty: Document = try SwiftSoup.parseBodyFragment(bodyHtml, baseUri)
|
||||
let cleaner: Cleaner = Cleaner(whitelist)
|
||||
let clean: Document = try cleaner.clean(dirty)
|
||||
clean.outputSettings(outputSettings)
|
||||
return try clean.body()?.html()
|
||||
}
|
||||
|
||||
/**
|
||||
Test if the input HTML has only tags and attributes allowed by the Whitelist. Useful for form validation. The input HTML should
|
||||
still be run through the cleaner to set up enforced attributes, and to tidy the output.
|
||||
@param bodyHtml HTML to test
|
||||
@param whitelist whitelist to test against
|
||||
@return true if no tags or attributes were removed; false otherwise
|
||||
@see #clean(String, org.jsoup.safety.Whitelist)
|
||||
*/
|
||||
public func isValid(_ bodyHtml: String, _ whitelist: Whitelist)throws->Bool {
|
||||
let dirty = try parseBodyFragment(bodyHtml, "")
|
||||
let cleaner = Cleaner(whitelist)
|
||||
return try cleaner.isValid(dirty)
|
||||
}
|
||||
Generated
+338
@@ -0,0 +1,338 @@
|
||||
//
|
||||
// Tag.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 15/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class Tag: Hashable {
|
||||
// map of known tags
|
||||
static var tags: Dictionary<String, Tag> = {
|
||||
do {
|
||||
return try Tag.initializeMaps()
|
||||
} catch {
|
||||
preconditionFailure("This method must be overridden")
|
||||
}
|
||||
return Dictionary<String, Tag>()
|
||||
}()
|
||||
|
||||
fileprivate var _tagName: String
|
||||
fileprivate var _tagNameNormal: String
|
||||
fileprivate var _isBlock: Bool = true // block or inline
|
||||
fileprivate var _formatAsBlock: Bool = true // should be formatted as a block
|
||||
fileprivate var _canContainBlock: Bool = true // Can this tag hold block level tags?
|
||||
fileprivate var _canContainInline: Bool = true // only pcdata if not
|
||||
fileprivate var _empty: Bool = false // can hold nothing e.g. img
|
||||
fileprivate var _selfClosing: Bool = false // can self close (<foo />). used for unknown tags that self close, without forcing them as empty.
|
||||
fileprivate var _preserveWhitespace: Bool = false // for pre, textarea, script etc
|
||||
fileprivate var _formList: Bool = false // a control that appears in forms: input, textarea, output etc
|
||||
fileprivate var _formSubmit: Bool = false // a control that can be submitted in a form: input etc
|
||||
|
||||
public init(_ tagName: String) {
|
||||
self._tagName = tagName
|
||||
self._tagNameNormal = tagName.lowercased()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this tag's name.
|
||||
*
|
||||
* @return the tag's name
|
||||
*/
|
||||
open func getName() -> String {
|
||||
return self._tagName
|
||||
}
|
||||
open func getNameNormal() -> String {
|
||||
return self._tagNameNormal
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Tag by name. If not previously defined (unknown), returns a new generic tag, that can do anything.
|
||||
* <p>
|
||||
* Pre-defined tags (P, DIV etc) will be ==, but unknown tags are not registered and will only .equals().
|
||||
* </p>
|
||||
*
|
||||
* @param tagName Name of tag, e.g. "p". Case insensitive.
|
||||
* @param settings used to control tag name sensitivity
|
||||
* @return The tag, either defined or new generic.
|
||||
*/
|
||||
public static func valueOf(_ tagName: String, _ settings: ParseSettings)throws->Tag {
|
||||
var tagName = tagName
|
||||
var tag: Tag? = Tag.tags[tagName]
|
||||
|
||||
if (tag == nil) {
|
||||
tagName = settings.normalizeTag(tagName)
|
||||
try Validate.notEmpty(string: tagName)
|
||||
tag = Tag.tags[tagName]
|
||||
|
||||
if (tag == nil) {
|
||||
// not defined: create default; go anywhere, do anything! (incl be inside a <p>)
|
||||
tag = Tag(tagName)
|
||||
tag!._isBlock = false
|
||||
tag!._canContainBlock = true
|
||||
}
|
||||
}
|
||||
return tag!
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Tag by name. If not previously defined (unknown), returns a new generic tag, that can do anything.
|
||||
* <p>
|
||||
* Pre-defined tags (P, DIV etc) will be ==, but unknown tags are not registered and will only .equals().
|
||||
* </p>
|
||||
*
|
||||
* @param tagName Name of tag, e.g. "p". <b>Case sensitive</b>.
|
||||
* @return The tag, either defined or new generic.
|
||||
*/
|
||||
public static func valueOf(_ tagName: String)throws->Tag {
|
||||
return try valueOf(tagName, ParseSettings.preserveCase)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if this is a block tag.
|
||||
*
|
||||
* @return if block tag
|
||||
*/
|
||||
open func isBlock() -> Bool {
|
||||
return _isBlock
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if this tag should be formatted as a block (or as inline)
|
||||
*
|
||||
* @return if should be formatted as block or inline
|
||||
*/
|
||||
open func formatAsBlock() -> Bool {
|
||||
return _formatAsBlock
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if this tag can contain block tags.
|
||||
*
|
||||
* @return if tag can contain block tags
|
||||
*/
|
||||
open func canContainBlock() -> Bool {
|
||||
return _canContainBlock
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if this tag is an inline tag.
|
||||
*
|
||||
* @return if this tag is an inline tag.
|
||||
*/
|
||||
open func isInline() -> Bool {
|
||||
return !_isBlock
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if this tag is a data only tag.
|
||||
*
|
||||
* @return if this tag is a data only tag
|
||||
*/
|
||||
open func isData() -> Bool {
|
||||
return !_canContainInline && !isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if this is an empty tag
|
||||
*
|
||||
* @return if this is an empty tag
|
||||
*/
|
||||
open func isEmpty() -> Bool {
|
||||
return _empty
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if this tag is self closing.
|
||||
*
|
||||
* @return if this tag should be output as self closing.
|
||||
*/
|
||||
open func isSelfClosing() -> Bool {
|
||||
return _empty || _selfClosing
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if this is a pre-defined tag, or was auto created on parsing.
|
||||
*
|
||||
* @return if a known tag
|
||||
*/
|
||||
open func isKnownTag() -> Bool {
|
||||
return Tag.tags[_tagName] != nil
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this tagname is a known tag.
|
||||
*
|
||||
* @param tagName name of tag
|
||||
* @return if known HTML tag
|
||||
*/
|
||||
public static func isKnownTag(_ tagName: String) -> Bool {
|
||||
return Tag.tags[tagName] != nil
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if this tag should preserve whitespace within child text nodes.
|
||||
*
|
||||
* @return if preserve whitepace
|
||||
*/
|
||||
public func preserveWhitespace() -> Bool {
|
||||
return _preserveWhitespace
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if this tag represents a control associated with a form. E.g. input, textarea, output
|
||||
* @return if associated with a form
|
||||
*/
|
||||
public func isFormListed() -> Bool {
|
||||
return _formList
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if this tag represents an element that should be submitted with a form. E.g. input, option
|
||||
* @return if submittable with a form
|
||||
*/
|
||||
public func isFormSubmittable() -> Bool {
|
||||
return _formSubmit
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setSelfClosing() -> Tag {
|
||||
_selfClosing = true
|
||||
return self
|
||||
}
|
||||
|
||||
/// 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.
|
||||
static public func ==(lhs: Tag, rhs: Tag) -> Bool {
|
||||
let this = lhs
|
||||
let o = rhs
|
||||
if (this === o) {return true}
|
||||
if (type(of: this) != type(of: o)) {return false}
|
||||
|
||||
let tag: Tag = o
|
||||
|
||||
if (lhs._tagName != tag._tagName) {return false}
|
||||
if (lhs._canContainBlock != tag._canContainBlock) {return false}
|
||||
if (lhs._canContainInline != tag._canContainInline) {return false}
|
||||
if (lhs._empty != tag._empty) {return false}
|
||||
if (lhs._formatAsBlock != tag._formatAsBlock) {return false}
|
||||
if (lhs._isBlock != tag._isBlock) {return false}
|
||||
if (lhs._preserveWhitespace != tag._preserveWhitespace) {return false}
|
||||
if (lhs._selfClosing != tag._selfClosing) {return false}
|
||||
if (lhs._formList != tag._formList) {return false}
|
||||
return lhs._formSubmit == tag._formSubmit
|
||||
}
|
||||
|
||||
public func equals(_ tag: Tag) -> Bool {
|
||||
return self == tag
|
||||
}
|
||||
|
||||
/// 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 var hashValue: Int {
|
||||
return _tagName.hashValue ^ _isBlock.hashValue ^ _formatAsBlock.hashValue ^ _canContainBlock.hashValue ^ _canContainInline.hashValue ^ _empty.hashValue ^ _selfClosing.hashValue ^ _preserveWhitespace.hashValue ^ _formList.hashValue ^ _formSubmit.hashValue
|
||||
}
|
||||
|
||||
open func toString() -> String {
|
||||
return _tagName
|
||||
}
|
||||
|
||||
// internal static initialisers:
|
||||
// prepped from http://www.w3.org/TR/REC-html40/sgml/dtd.html and other sources
|
||||
private static let blockTags: [String] = [
|
||||
"html", "head", "body", "frameset", "script", "noscript", "style", "meta", "link", "title", "frame",
|
||||
"noframes", "section", "nav", "aside", "hgroup", "header", "footer", "p", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"ul", "ol", "pre", "div", "blockquote", "hr", "address", "figure", "figcaption", "form", "fieldset", "ins",
|
||||
"del", "s", "dl", "dt", "dd", "li", "table", "caption", "thead", "tfoot", "tbody", "colgroup", "col", "tr", "th",
|
||||
"td", "video", "audio", "canvas", "details", "menu", "plaintext", "template", "article", "main",
|
||||
"svg", "math"
|
||||
]
|
||||
private static let inlineTags: [String] = [
|
||||
"object", "base", "font", "tt", "i", "b", "u", "big", "small", "em", "strong", "dfn", "code", "samp", "kbd",
|
||||
"var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "a", "img", "br", "wbr", "map", "q",
|
||||
"sub", "sup", "bdo", "iframe", "embed", "span", "input", "select", "textarea", "label", "button", "optgroup",
|
||||
"option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track",
|
||||
"summary", "command", "device", "area", "basefont", "bgsound", "menuitem", "param", "source", "track",
|
||||
"data", "bdi"
|
||||
]
|
||||
private static let emptyTags: [String] = [
|
||||
"meta", "link", "base", "frame", "img", "br", "wbr", "embed", "hr", "input", "keygen", "col", "command",
|
||||
"device", "area", "basefont", "bgsound", "menuitem", "param", "source", "track"
|
||||
]
|
||||
private static let formatAsInlineTags: [String] = [
|
||||
"title", "a", "p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "address", "li", "th", "td", "script", "style",
|
||||
"ins", "del", "s"
|
||||
]
|
||||
private static let preserveWhitespaceTags: [String] = [
|
||||
"pre", "plaintext", "title", "textarea"
|
||||
// script is not here as it is a data node, which always preserve whitespace
|
||||
]
|
||||
// todo: I think we just need submit tags, and can scrub listed
|
||||
private static let formListedTags: [String] = [
|
||||
"button", "fieldset", "input", "keygen", "object", "output", "select", "textarea"
|
||||
]
|
||||
private static let formSubmitTags: [String] = [
|
||||
"input", "keygen", "object", "select", "textarea"
|
||||
]
|
||||
|
||||
static private func initializeMaps()throws->Dictionary<String, Tag> {
|
||||
var dict = Dictionary<String, Tag>()
|
||||
|
||||
// creates
|
||||
for tagName in blockTags {
|
||||
let tag = Tag(tagName)
|
||||
dict[tag._tagName] = tag
|
||||
}
|
||||
for tagName in inlineTags {
|
||||
let tag = Tag(tagName)
|
||||
tag._isBlock = false
|
||||
tag._canContainBlock = false
|
||||
tag._formatAsBlock = false
|
||||
dict[tag._tagName] = tag
|
||||
}
|
||||
|
||||
// mods:
|
||||
for tagName in emptyTags {
|
||||
let tag = dict[tagName]
|
||||
try Validate.notNull(obj: tag)
|
||||
tag?._canContainBlock = false
|
||||
tag?._canContainInline = false
|
||||
tag?._empty = true
|
||||
}
|
||||
|
||||
for tagName in formatAsInlineTags {
|
||||
let tag = dict[tagName]
|
||||
try Validate.notNull(obj: tag)
|
||||
tag?._formatAsBlock = false
|
||||
}
|
||||
|
||||
for tagName in preserveWhitespaceTags {
|
||||
let tag = dict[tagName]
|
||||
try Validate.notNull(obj: tag)
|
||||
tag?._preserveWhitespace = true
|
||||
}
|
||||
|
||||
for tagName in formListedTags {
|
||||
let tag = dict[tagName]
|
||||
try Validate.notNull(obj: tag)
|
||||
tag?._formList = true
|
||||
}
|
||||
|
||||
for tagName in formSubmitTags {
|
||||
let tag = dict[tagName]
|
||||
try Validate.notNull(obj: tag)
|
||||
tag?._formSubmit = true
|
||||
}
|
||||
return dict
|
||||
}
|
||||
}
|
||||
Generated
+199
@@ -0,0 +1,199 @@
|
||||
//
|
||||
// TextNode.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A text node.
|
||||
*/
|
||||
open class TextNode: Node {
|
||||
/*
|
||||
TextNode is a node, and so by default comes with attributes and children. The attributes are seldom used, but use
|
||||
memory, and the child nodes are never used. So we don't have them, and override accessors to attributes to create
|
||||
them as needed on the fly.
|
||||
*/
|
||||
private static let TEXT_KEY: String = "text"
|
||||
var _text: String
|
||||
|
||||
/**
|
||||
Create a new TextNode representing the supplied (unencoded) text).
|
||||
|
||||
@param text raw text
|
||||
@param baseUri base uri
|
||||
@see #createFromEncoded(String, String)
|
||||
*/
|
||||
public init(_ text: String, _ baseUri: String?) {
|
||||
self._text = text
|
||||
super.init()
|
||||
self.baseUri = baseUri
|
||||
|
||||
}
|
||||
|
||||
open override func nodeName() -> String {
|
||||
return "#text"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text content of this text node.
|
||||
* @return Unencoded, normalised text.
|
||||
* @see TextNode#getWholeText()
|
||||
*/
|
||||
open func text() -> String {
|
||||
return TextNode.normaliseWhitespace(getWholeText())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text content of this text node.
|
||||
* @param text unencoded text
|
||||
* @return this, for chaining
|
||||
*/
|
||||
@discardableResult
|
||||
public func text(_ text: String) -> TextNode {
|
||||
self._text = text
|
||||
guard let attributes = attributes else {
|
||||
return self
|
||||
}
|
||||
do {
|
||||
try attributes.put(TextNode.TEXT_KEY, text)
|
||||
} catch {
|
||||
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Get the (unencoded) text of this text node, including any newlines and spaces present in the original.
|
||||
@return text
|
||||
*/
|
||||
open func getWholeText() -> String {
|
||||
return attributes == nil ? _text : attributes!.get(key: TextNode.TEXT_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
Test if this text node is blank -- that is, empty or only whitespace (including newlines).
|
||||
@return true if this document is empty or only whitespace, false if it contains any text content.
|
||||
*/
|
||||
open func isBlank() -> Bool {
|
||||
return StringUtil.isBlank(getWholeText())
|
||||
}
|
||||
|
||||
/**
|
||||
* Split this text node into two nodes at the specified string offset. After splitting, this node will contain the
|
||||
* original text up to the offset, and will have a new text node sibling containing the text after the offset.
|
||||
* @param offset string offset point to split node at.
|
||||
* @return the newly created text node containing the text after the offset.
|
||||
*/
|
||||
open func splitText(_ offset: Int)throws->TextNode {
|
||||
try Validate.isTrue(val: offset >= 0, msg: "Split offset must be not be negative")
|
||||
try Validate.isTrue(val: offset < _text.count, msg: "Split offset must not be greater than current text length")
|
||||
|
||||
let head: String = getWholeText().substring(0, offset)
|
||||
let tail: String = getWholeText().substring(offset)
|
||||
text(head)
|
||||
let tailNode: TextNode = TextNode(tail, self.getBaseUri())
|
||||
if (parent() != nil) {
|
||||
try parent()?.addChildren(siblingIndex+1, tailNode)
|
||||
}
|
||||
return tailNode
|
||||
}
|
||||
|
||||
override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings)throws {
|
||||
if (out.prettyPrint() &&
|
||||
((siblingIndex == 0 && (parentNode as? Element) != nil && (parentNode as! Element).tag().formatAsBlock() && !isBlank()) ||
|
||||
(out.outline() && siblingNodes().count > 0 && !isBlank()) )) {
|
||||
indent(accum, depth, out)
|
||||
}
|
||||
|
||||
let par: Element? = parent() as? Element
|
||||
let normaliseWhite = out.prettyPrint() && par != nil && !Element.preserveWhitespace(par!)
|
||||
|
||||
Entities.escape(accum, getWholeText(), out, false, normaliseWhite, false)
|
||||
}
|
||||
|
||||
override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new TextNode from HTML encoded (aka escaped) data.
|
||||
* @param encodedText Text containing encoded HTML (e.g. &lt;)
|
||||
* @param baseUri Base uri
|
||||
* @return TextNode containing unencoded data (e.g. <)
|
||||
*/
|
||||
public static func createFromEncoded(_ encodedText: String, _ baseUri: String)throws->TextNode {
|
||||
let text: String = try Entities.unescape(encodedText)
|
||||
return TextNode(text, baseUri)
|
||||
}
|
||||
|
||||
static public func normaliseWhitespace(_ text: String) -> String {
|
||||
let _text = StringUtil.normaliseWhitespace(text)
|
||||
return _text
|
||||
}
|
||||
|
||||
static public func stripLeadingWhitespace(_ text: String) -> String {
|
||||
return text.replaceFirst(of: "^\\s+", with: "")
|
||||
//return text.replaceFirst("^\\s+", "")
|
||||
}
|
||||
|
||||
static public func lastCharIsWhitespace(_ sb: StringBuilder) -> Bool {
|
||||
return sb.toString().last == " "
|
||||
}
|
||||
|
||||
// attribute fiddling. create on first access.
|
||||
private func ensureAttributes() {
|
||||
if (attributes == nil) {
|
||||
attributes = Attributes()
|
||||
do {
|
||||
try attributes?.put(TextNode.TEXT_KEY, _text)
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
open override func attr(_ attributeKey: String)throws->String {
|
||||
ensureAttributes()
|
||||
return try super.attr(attributeKey)
|
||||
}
|
||||
|
||||
open override func getAttributes() -> Attributes {
|
||||
ensureAttributes()
|
||||
return super.getAttributes()!
|
||||
}
|
||||
|
||||
open override func attr(_ attributeKey: String, _ attributeValue: String)throws->Node {
|
||||
ensureAttributes()
|
||||
return try super.attr(attributeKey, attributeValue)
|
||||
}
|
||||
|
||||
open override func hasAttr(_ attributeKey: String) -> Bool {
|
||||
ensureAttributes()
|
||||
return super.hasAttr(attributeKey)
|
||||
}
|
||||
|
||||
open override func removeAttr(_ attributeKey: String)throws->Node {
|
||||
ensureAttributes()
|
||||
return try super.removeAttr(attributeKey)
|
||||
}
|
||||
|
||||
open override func absUrl(_ attributeKey: String)throws->String {
|
||||
ensureAttributes()
|
||||
return try super.absUrl(attributeKey)
|
||||
}
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let clone = TextNode(_text, baseUri)
|
||||
return super.copy(clone: clone)
|
||||
}
|
||||
|
||||
public override func copy(parent: Node?) -> Node {
|
||||
let clone = TextNode(_text, baseUri)
|
||||
return super.copy(clone: clone, parent: parent)
|
||||
}
|
||||
|
||||
public override func copy(clone: Node, parent: Node?) -> Node {
|
||||
return super.copy(clone: clone, parent: parent)
|
||||
}
|
||||
}
|
||||
Generated
+396
@@ -0,0 +1,396 @@
|
||||
//
|
||||
// Token.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 18/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class Token {
|
||||
var type: TokenType = TokenType.Doctype
|
||||
|
||||
private init() {
|
||||
}
|
||||
|
||||
func tokenType() -> String {
|
||||
return String(describing: Swift.type(of: self))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the data represent by this token, for reuse. Prevents the need to create transfer objects for every
|
||||
* piece of data, which immediately get GCed.
|
||||
*/
|
||||
@discardableResult
|
||||
public func reset() -> Token {
|
||||
preconditionFailure("This method must be overridden")
|
||||
}
|
||||
|
||||
static func reset(_ sb: StringBuilder) {
|
||||
sb.clear()
|
||||
}
|
||||
|
||||
open func toString()throws->String {
|
||||
return String(describing: Swift.type(of: self))
|
||||
}
|
||||
|
||||
final class Doctype: Token {
|
||||
let name: StringBuilder = StringBuilder()
|
||||
var pubSysKey: String?
|
||||
let publicIdentifier: StringBuilder = StringBuilder()
|
||||
let systemIdentifier: StringBuilder = StringBuilder()
|
||||
var forceQuirks: Bool = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
type = TokenType.Doctype
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
override func reset() -> Token {
|
||||
Token.reset(name)
|
||||
pubSysKey = nil
|
||||
Token.reset(publicIdentifier)
|
||||
Token.reset(systemIdentifier)
|
||||
forceQuirks = false
|
||||
return self
|
||||
}
|
||||
|
||||
func getName() -> String {
|
||||
return name.toString()
|
||||
}
|
||||
|
||||
func getPubSysKey() -> String? {
|
||||
return pubSysKey
|
||||
}
|
||||
|
||||
func getPublicIdentifier() -> String {
|
||||
return publicIdentifier.toString()
|
||||
}
|
||||
|
||||
public func getSystemIdentifier() -> String {
|
||||
return systemIdentifier.toString()
|
||||
}
|
||||
|
||||
public func isForceQuirks() -> Bool {
|
||||
return forceQuirks
|
||||
}
|
||||
}
|
||||
|
||||
class Tag: Token {
|
||||
public var _tagName: String?
|
||||
public var _normalName: String? // lc version of tag name, for case insensitive tree build
|
||||
private var _pendingAttributeName: String? // attribute names are generally caught in one hop, not accumulated
|
||||
private let _pendingAttributeValue: StringBuilder = StringBuilder() // but values are accumulated, from e.g. & in hrefs
|
||||
private var _pendingAttributeValueS: String? // try to get attr vals in one shot, vs Builder
|
||||
private var _hasEmptyAttributeValue: Bool = false // distinguish boolean attribute from empty string value
|
||||
private var _hasPendingAttributeValue: Bool = false
|
||||
public var _selfClosing: Bool = false
|
||||
// start tags get attributes on construction. End tags get attributes on first new attribute (but only for parser convenience, not used).
|
||||
public var _attributes: Attributes = Attributes()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
override func reset() -> Tag {
|
||||
_tagName = nil
|
||||
_normalName = nil
|
||||
_pendingAttributeName = nil
|
||||
Token.reset(_pendingAttributeValue)
|
||||
_pendingAttributeValueS = nil
|
||||
_hasEmptyAttributeValue = false
|
||||
_hasPendingAttributeValue = false
|
||||
_selfClosing = false
|
||||
_attributes = Attributes()
|
||||
return self
|
||||
}
|
||||
|
||||
func newAttribute()throws {
|
||||
// if (_attributes == nil){
|
||||
// _attributes = Attributes()
|
||||
// }
|
||||
|
||||
if (_pendingAttributeName != nil) {
|
||||
var attribute: Attribute
|
||||
if (_hasPendingAttributeValue) {
|
||||
attribute = try Attribute(key: _pendingAttributeName!, value: _pendingAttributeValue.length > 0 ? _pendingAttributeValue.toString() : _pendingAttributeValueS!)
|
||||
} else if (_hasEmptyAttributeValue) {
|
||||
attribute = try Attribute(key: _pendingAttributeName!, value: "")
|
||||
} else {
|
||||
attribute = try BooleanAttribute(key: _pendingAttributeName!)
|
||||
}
|
||||
_attributes.put(attribute: attribute)
|
||||
}
|
||||
_pendingAttributeName = nil
|
||||
_hasEmptyAttributeValue = false
|
||||
_hasPendingAttributeValue = false
|
||||
Token.reset(_pendingAttributeValue)
|
||||
_pendingAttributeValueS = nil
|
||||
}
|
||||
|
||||
func finaliseTag()throws {
|
||||
// finalises for emit
|
||||
if (_pendingAttributeName != nil) {
|
||||
// todo: check if attribute name exists; if so, drop and error
|
||||
try newAttribute()
|
||||
}
|
||||
}
|
||||
|
||||
func name()throws->String { // preserves case, for input into Tag.valueOf (which may drop case)
|
||||
try Validate.isFalse(val: _tagName == nil || _tagName!.unicodeScalars.count == 0)
|
||||
return _tagName!
|
||||
}
|
||||
|
||||
func normalName() -> String? { // loses case, used in tree building for working out where in tree it should go
|
||||
return _normalName
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func name(_ name: String) -> Tag {
|
||||
_tagName = name
|
||||
_normalName = name.lowercased()
|
||||
return self
|
||||
}
|
||||
|
||||
func isSelfClosing() -> Bool {
|
||||
return _selfClosing
|
||||
}
|
||||
|
||||
func getAttributes() -> Attributes {
|
||||
return _attributes
|
||||
}
|
||||
|
||||
// these appenders are rarely hit in not null state-- caused by null chars.
|
||||
func appendTagName(_ append: String) {
|
||||
_tagName = _tagName == nil ? append : _tagName!.appending(append)
|
||||
_normalName = _tagName?.lowercased()
|
||||
}
|
||||
|
||||
func appendTagName(_ append: UnicodeScalar) {
|
||||
appendTagName("\(append)")
|
||||
}
|
||||
|
||||
func appendAttributeName(_ append: String) {
|
||||
_pendingAttributeName = _pendingAttributeName == nil ? append : _pendingAttributeName?.appending(append)
|
||||
}
|
||||
|
||||
func appendAttributeName(_ append: UnicodeScalar) {
|
||||
appendAttributeName("\(append)")
|
||||
}
|
||||
|
||||
func appendAttributeValue(_ append: String) {
|
||||
ensureAttributeValue()
|
||||
if (_pendingAttributeValue.length == 0) {
|
||||
_pendingAttributeValueS = append
|
||||
} else {
|
||||
_pendingAttributeValue.append(append)
|
||||
}
|
||||
}
|
||||
|
||||
func appendAttributeValue(_ append: UnicodeScalar) {
|
||||
ensureAttributeValue()
|
||||
_pendingAttributeValue.appendCodePoint(append)
|
||||
}
|
||||
|
||||
func appendAttributeValue(_ append: [UnicodeScalar]) {
|
||||
ensureAttributeValue()
|
||||
_pendingAttributeValue.appendCodePoints(append)
|
||||
}
|
||||
|
||||
func appendAttributeValue(_ appendCodepoints: [Int]) {
|
||||
ensureAttributeValue()
|
||||
for codepoint in appendCodepoints {
|
||||
_pendingAttributeValue.appendCodePoint(UnicodeScalar(codepoint)!)
|
||||
}
|
||||
}
|
||||
|
||||
func setEmptyAttributeValue() {
|
||||
_hasEmptyAttributeValue = true
|
||||
}
|
||||
|
||||
private func ensureAttributeValue() {
|
||||
_hasPendingAttributeValue = true
|
||||
// if on second hit, we'll need to move to the builder
|
||||
if (_pendingAttributeValueS != nil) {
|
||||
_pendingAttributeValue.append(_pendingAttributeValueS!)
|
||||
_pendingAttributeValueS = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class StartTag: Tag {
|
||||
override init() {
|
||||
super.init()
|
||||
_attributes = Attributes()
|
||||
type = TokenType.StartTag
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
override func reset() -> Tag {
|
||||
super.reset()
|
||||
_attributes = Attributes()
|
||||
// todo - would prefer these to be null, but need to check Element assertions
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func nameAttr(_ name: String, _ attributes: Attributes) -> StartTag {
|
||||
self._tagName = name
|
||||
self._attributes = attributes
|
||||
_normalName = _tagName?.lowercased()
|
||||
return self
|
||||
}
|
||||
|
||||
public override func toString()throws->String {
|
||||
if (_attributes.size() > 0) {
|
||||
return try "<" + (name()) + " " + (_attributes.toString()) + ">"
|
||||
} else {
|
||||
return try "<" + name() + ">"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class EndTag: Tag {
|
||||
override init() {
|
||||
super.init()
|
||||
type = TokenType.EndTag
|
||||
}
|
||||
|
||||
public override func toString()throws->String {
|
||||
return "</" + (try name()) + ">"
|
||||
}
|
||||
}
|
||||
|
||||
final class Comment: Token {
|
||||
let data: StringBuilder = StringBuilder()
|
||||
var bogus: Bool = false
|
||||
|
||||
@discardableResult
|
||||
override func reset() -> Token {
|
||||
Token.reset(data)
|
||||
bogus = false
|
||||
return self
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
type = TokenType.Comment
|
||||
}
|
||||
|
||||
func getData() -> String {
|
||||
return data.toString()
|
||||
}
|
||||
|
||||
public override func toString()throws->String {
|
||||
return "<!--" + getData() + "-->"
|
||||
}
|
||||
}
|
||||
|
||||
final class Char: Token {
|
||||
public var data: String?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
type = TokenType.Char
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
override func reset() -> Token {
|
||||
data = nil
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func data(_ data: String) -> Char {
|
||||
self.data = data
|
||||
return self
|
||||
}
|
||||
|
||||
func getData() -> String? {
|
||||
return data
|
||||
}
|
||||
|
||||
public override func toString()throws->String {
|
||||
try Validate.notNull(obj: data)
|
||||
return getData()!
|
||||
}
|
||||
}
|
||||
|
||||
final class EOF: Token {
|
||||
override init() {
|
||||
super.init()
|
||||
type = Token.TokenType.EOF
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
override func reset() -> Token {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
func isDoctype() -> Bool {
|
||||
return type == TokenType.Doctype
|
||||
}
|
||||
|
||||
func asDoctype() -> Doctype {
|
||||
return self as! Doctype
|
||||
}
|
||||
|
||||
func isStartTag() -> Bool {
|
||||
return type == TokenType.StartTag
|
||||
}
|
||||
|
||||
func asStartTag() -> StartTag {
|
||||
return self as! StartTag
|
||||
}
|
||||
|
||||
func isEndTag() -> Bool {
|
||||
return type == TokenType.EndTag
|
||||
}
|
||||
|
||||
func asEndTag() -> EndTag {
|
||||
return self as! EndTag
|
||||
}
|
||||
|
||||
func isComment() -> Bool {
|
||||
return type == TokenType.Comment
|
||||
}
|
||||
|
||||
func asComment() -> Comment {
|
||||
return self as! Comment
|
||||
}
|
||||
|
||||
func isCharacter() -> Bool {
|
||||
return type == TokenType.Char
|
||||
}
|
||||
|
||||
func asCharacter() -> Char {
|
||||
return self as! Char
|
||||
}
|
||||
|
||||
func isEOF() -> Bool {
|
||||
return type == TokenType.EOF
|
||||
}
|
||||
|
||||
public enum TokenType {
|
||||
case Doctype
|
||||
case StartTag
|
||||
case EndTag
|
||||
case Comment
|
||||
case Char
|
||||
case EOF
|
||||
}
|
||||
}
|
||||
|
||||
extension Token: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
do {
|
||||
return try self.toString()
|
||||
} catch {
|
||||
return "Error while get string debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+428
@@ -0,0 +1,428 @@
|
||||
//
|
||||
// TokenQueue.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 13/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class TokenQueue {
|
||||
private var queue: String
|
||||
private var pos: Int = 0
|
||||
private static let empty: Character = Character(UnicodeScalar(0))
|
||||
private static let ESC: Character = "\\" // escape char for chomp balanced.
|
||||
|
||||
/**
|
||||
Create a new TokenQueue.
|
||||
@param data string of data to back queue.
|
||||
*/
|
||||
public init (_ data: String) {
|
||||
queue = data
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the queue empty?
|
||||
* @return true if no data left in queue.
|
||||
*/
|
||||
open func isEmpty() -> Bool {
|
||||
return remainingLength() == 0
|
||||
}
|
||||
|
||||
private func remainingLength() -> Int {
|
||||
return queue.count - pos
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves but does not remove the first character from the queue.
|
||||
* @return First character, or 0 if empty.
|
||||
*/
|
||||
open func peek() -> Character {
|
||||
return isEmpty() ? Character(UnicodeScalar(0)) : queue[pos]
|
||||
}
|
||||
|
||||
/**
|
||||
Add a character to the start of the queue (will be the next character retrieved).
|
||||
@param c character to add
|
||||
*/
|
||||
open func addFirst(_ c: Character) {
|
||||
addFirst(String(c))
|
||||
}
|
||||
|
||||
/**
|
||||
Add a string to the start of the queue.
|
||||
@param seq string to add.
|
||||
*/
|
||||
open func addFirst(_ seq: String) {
|
||||
// not very performant, but an edge case
|
||||
queue = seq + queue.substring(pos)
|
||||
pos = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the next characters on the queue match the sequence. Case insensitive.
|
||||
* @param seq String to check queue for.
|
||||
* @return true if the next characters match.
|
||||
*/
|
||||
open func matches(_ seq: String) -> Bool {
|
||||
return queue.regionMatches(true, pos, seq, 0, seq.count)
|
||||
}
|
||||
|
||||
/**
|
||||
* Case sensitive match test.
|
||||
* @param seq string to case sensitively check for
|
||||
* @return true if matched, false if not
|
||||
*/
|
||||
open func matchesCS(_ seq: String) -> Bool {
|
||||
return queue.startsWith(seq, pos)
|
||||
}
|
||||
|
||||
/**
|
||||
Tests if the next characters match any of the sequences. Case insensitive.
|
||||
@param seq list of strings to case insensitively check for
|
||||
@return true of any matched, false if none did
|
||||
*/
|
||||
open func matchesAny(_ seq: [String]) -> Bool {
|
||||
for s in seq {
|
||||
if (matches(s)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
open func matchesAny(_ seq: String...) -> Bool {
|
||||
return matchesAny(seq)
|
||||
}
|
||||
|
||||
open func matchesAny(_ seq: Character...) -> Bool {
|
||||
if (isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
for c in seq {
|
||||
if (queue[pos] as Character == c) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
open func matchesStartTag() -> Bool {
|
||||
// micro opt for matching "<x"
|
||||
return (remainingLength() >= 2 && queue[pos] as Character == "<" && Character.isLetter(queue.charAt(pos+1)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the queue matches the sequence (as with match), and if they do, removes the matched string from the
|
||||
* queue.
|
||||
* @param seq String to search for, and if found, remove from queue.
|
||||
* @return true if found and removed, false if not found.
|
||||
*/
|
||||
@discardableResult
|
||||
open func matchChomp(_ seq: String) -> Bool {
|
||||
if (matches(seq)) {
|
||||
pos += seq.count
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Tests if queue starts with a whitespace character.
|
||||
@return if starts with whitespace
|
||||
*/
|
||||
open func matchesWhitespace() -> Bool {
|
||||
return !isEmpty() && StringUtil.isWhitespace(queue.charAt(pos))
|
||||
}
|
||||
|
||||
/**
|
||||
Test if the queue matches a word character (letter or digit).
|
||||
@return if matches a word character
|
||||
*/
|
||||
open func matchesWord() -> Bool {
|
||||
return !isEmpty() && (Character.isLetterOrDigit(queue.charAt(pos)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the next character off the queue.
|
||||
*/
|
||||
open func advance() {
|
||||
|
||||
if (!isEmpty()) {pos+=1}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume one character off queue.
|
||||
* @return first character on queue.
|
||||
*/
|
||||
open func consume() -> Character {
|
||||
let i = pos
|
||||
pos+=1
|
||||
return queue.charAt(i)
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the supplied sequence of the queue. If the queue does not start with the supplied sequence, will
|
||||
* throw an illegal state exception -- but you should be running match() against that condition.
|
||||
<p>
|
||||
Case insensitive.
|
||||
* @param seq sequence to remove from head of queue.
|
||||
*/
|
||||
open func consume(_ seq: String)throws {
|
||||
if (!matches(seq)) {
|
||||
//throw new IllegalStateException("Queue did not match expected sequence")
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Queue did not match expected sequence")
|
||||
}
|
||||
let len = seq.count
|
||||
if (len > remainingLength()) {
|
||||
//throw new IllegalStateException("Queue not long enough to consume sequence")
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Queue not long enough to consume sequence")
|
||||
}
|
||||
|
||||
pos += len
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls a string off the queue, up to but exclusive of the match sequence, or to the queue running out.
|
||||
* @param seq String to end on (and not include in return, but leave on queue). <b>Case sensitive.</b>
|
||||
* @return The matched data consumed from queue.
|
||||
*/
|
||||
@discardableResult
|
||||
open func consumeTo(_ seq: String) -> String {
|
||||
let offset = queue.indexOf(seq, pos)
|
||||
if (offset != -1) {
|
||||
let consumed = queue.substring(pos, offset-pos)
|
||||
pos += consumed.count
|
||||
return consumed
|
||||
} else {
|
||||
//return remainder()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
open func consumeToIgnoreCase(_ seq: String) -> String {
|
||||
let start = pos
|
||||
let first = seq.substring(0, 1)
|
||||
let canScan = first.lowercased() == first.uppercased() // if first is not cased, use index of
|
||||
while (!isEmpty()) {
|
||||
if (matches(seq)) {
|
||||
break
|
||||
}
|
||||
if (canScan) {
|
||||
let skip = queue.indexOf(first, pos) - pos
|
||||
if (skip == 0) { // this char is the skip char, but not match, so force advance of pos
|
||||
pos+=1
|
||||
} else if (skip < 0) { // no chance of finding, grab to end
|
||||
pos = queue.count
|
||||
} else {
|
||||
pos += skip
|
||||
}
|
||||
} else {
|
||||
pos+=1
|
||||
}
|
||||
}
|
||||
|
||||
return queue.substring(start, pos-start)
|
||||
}
|
||||
|
||||
/**
|
||||
Consumes to the first sequence provided, or to the end of the queue. Leaves the terminator on the queue.
|
||||
@param seq any number of terminators to consume to. <b>Case insensitive.</b>
|
||||
@return consumed string
|
||||
*/
|
||||
// todo: method name. not good that consumeTo cares for case, and consume to any doesn't. And the only use for this
|
||||
// is is a case sensitive time...
|
||||
open func consumeToAny(_ seq: String...) -> String {
|
||||
return consumeToAny(seq)
|
||||
}
|
||||
open func consumeToAny(_ seq: [String]) -> String {
|
||||
let start = pos
|
||||
while (!isEmpty() && !matchesAny(seq)) {
|
||||
pos+=1
|
||||
}
|
||||
|
||||
return queue.substring(start, pos-start)
|
||||
}
|
||||
/**
|
||||
* Pulls a string off the queue (like consumeTo), and then pulls off the matched string (but does not return it).
|
||||
* <p>
|
||||
* If the queue runs out of characters before finding the seq, will return as much as it can (and queue will go
|
||||
* isEmpty() == true).
|
||||
* @param seq String to match up to, and not include in return, and to pull off queue. <b>Case sensitive.</b>
|
||||
* @return Data matched from queue.
|
||||
*/
|
||||
open func chompTo(_ seq: String) -> String {
|
||||
let data = consumeTo(seq)
|
||||
matchChomp(seq)
|
||||
return data
|
||||
}
|
||||
|
||||
open func chompToIgnoreCase(_ seq: String) -> String {
|
||||
let data = consumeToIgnoreCase(seq) // case insensitive scan
|
||||
matchChomp(seq)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls a balanced string off the queue. E.g. if queue is "(one (two) three) four", (,) will return "one (two) three",
|
||||
* and leave " four" on the queue. Unbalanced openers and closers can quoted (with ' or ") or escaped (with \). Those escapes will be left
|
||||
* in the returned string, which is suitable for regexes (where we need to preserve the escape), but unsuitable for
|
||||
* contains text strings; use unescape for that.
|
||||
* @param open opener
|
||||
* @param close closer
|
||||
* @return data matched from the queue
|
||||
*/
|
||||
open func chompBalanced(_ open: Character, _ close: Character) -> String {
|
||||
var start = -1
|
||||
var end = -1
|
||||
var depth = 0
|
||||
var last: Character = TokenQueue.empty
|
||||
var inQuote = false
|
||||
|
||||
repeat {
|
||||
if (isEmpty()) {break}
|
||||
let c = consume()
|
||||
if (last == TokenQueue.empty || last != TokenQueue.ESC) {
|
||||
if ((c=="'" || c=="\"") && c != open) {
|
||||
inQuote = !inQuote
|
||||
}
|
||||
if (inQuote) {
|
||||
continue
|
||||
}
|
||||
if (c==open) {
|
||||
depth+=1
|
||||
if (start == -1) {
|
||||
start = pos
|
||||
}
|
||||
} else if (c==close) {
|
||||
depth-=1
|
||||
}
|
||||
}
|
||||
|
||||
if (depth > 0 && last != TokenQueue.empty) {
|
||||
end = pos // don't include the outer match pair in the return
|
||||
}
|
||||
last = c
|
||||
} while (depth > 0)
|
||||
return (end >= 0) ? queue.substring(start, end-start) : ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescaped a \ escaped string.
|
||||
* @param in backslash escaped string
|
||||
* @return unescaped string
|
||||
*/
|
||||
public static func unescape(_ input: String) -> String {
|
||||
let out = StringBuilder()
|
||||
var last = empty
|
||||
for c in input {
|
||||
if (c == ESC) {
|
||||
if (last != empty && last == TokenQueue.ESC) {
|
||||
out.append(c)
|
||||
}
|
||||
} else {
|
||||
out.append(c)
|
||||
}
|
||||
last = c
|
||||
}
|
||||
return out.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls the next run of whitespace characters of the queue.
|
||||
* @return Whether consuming whitespace or not
|
||||
*/
|
||||
@discardableResult
|
||||
open func consumeWhitespace() -> Bool {
|
||||
var seen = false
|
||||
while (matchesWhitespace()) {
|
||||
pos+=1
|
||||
seen = true
|
||||
}
|
||||
return seen
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next run of word type (letter or digit) off the queue.
|
||||
* @return String of word characters from queue, or empty string if none.
|
||||
*/
|
||||
@discardableResult
|
||||
open func consumeWord() -> String {
|
||||
let start = pos
|
||||
while (matchesWord()) {
|
||||
pos+=1
|
||||
}
|
||||
return queue.substring(start, pos-start)
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume an tag name off the queue (word or :, _, -)
|
||||
*
|
||||
* @return tag name
|
||||
*/
|
||||
open func consumeTagName() -> String {
|
||||
let start = pos
|
||||
while (!isEmpty() && (matchesWord() || matchesAny(":", "_", "-"))) {
|
||||
pos+=1
|
||||
}
|
||||
|
||||
return queue.substring(start, pos-start)
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume a CSS element selector (tag name, but | instead of : for namespaces (or *| for wildcard namespace), to not conflict with :pseudo selects).
|
||||
*
|
||||
* @return tag name
|
||||
*/
|
||||
open func consumeElementSelector() -> String {
|
||||
let start = pos
|
||||
while (!isEmpty() && (matchesWord() || matchesAny("*|", "|", "_", "-"))) {
|
||||
pos+=1
|
||||
}
|
||||
|
||||
return queue.substring(start, pos-start)
|
||||
}
|
||||
|
||||
/**
|
||||
Consume a CSS identifier (ID or class) off the queue (letter, digit, -, _)
|
||||
http://www.w3.org/TR/CSS2/syndata.html#value-def-identifier
|
||||
@return identifier
|
||||
*/
|
||||
open func consumeCssIdentifier() -> String {
|
||||
let start = pos
|
||||
while (!isEmpty() && (matchesWord() || matchesAny("-", "_"))) {
|
||||
pos+=1
|
||||
}
|
||||
|
||||
return queue.substring(start, pos-start)
|
||||
}
|
||||
|
||||
/**
|
||||
Consume an attribute key off the queue (letter, digit, -, _, :")
|
||||
@return attribute key
|
||||
*/
|
||||
open func consumeAttributeKey() -> String {
|
||||
let start = pos
|
||||
while (!isEmpty() && (matchesWord() || matchesAny("-", "_", ":"))) {
|
||||
pos+=1
|
||||
}
|
||||
|
||||
return queue.substring(start, pos-start)
|
||||
}
|
||||
|
||||
/**
|
||||
Consume and return whatever is left on the queue.
|
||||
@return remained of queue.
|
||||
*/
|
||||
open func remainder() -> String {
|
||||
let remainder = queue.substring(pos, queue.count-pos)
|
||||
pos = queue.count
|
||||
return remainder
|
||||
}
|
||||
|
||||
open func toString() -> String {
|
||||
return queue.substring(pos)
|
||||
}
|
||||
}
|
||||
Generated
+311
@@ -0,0 +1,311 @@
|
||||
//
|
||||
// Tokeniser.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 19/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class Tokeniser {
|
||||
static let replacementChar: UnicodeScalar = "\u{FFFD}" // replaces null character
|
||||
private static let notCharRefCharsSorted: [UnicodeScalar] = [UnicodeScalar.BackslashT, "\n", "\r", UnicodeScalar.BackslashF, " ", "<", UnicodeScalar.Ampersand].sorted()
|
||||
|
||||
private let reader: CharacterReader // html input
|
||||
private let errors: ParseErrorList? // errors found while tokenising
|
||||
|
||||
private var state: TokeniserState = TokeniserState.Data // current tokenisation state
|
||||
private var emitPending: Token? // the token we are about to emit on next read
|
||||
private var isEmitPending: Bool = false
|
||||
private var charsString: String? // characters pending an emit. Will fall to charsBuilder if more than one
|
||||
private let charsBuilder: StringBuilder = StringBuilder(1024) // buffers characters to output as one token, if more than one emit per read
|
||||
let dataBuffer: StringBuilder = StringBuilder(1024) // buffers data looking for </script>
|
||||
|
||||
var tagPending: Token.Tag = Token.Tag() // tag we are building up
|
||||
let startPending: Token.StartTag = Token.StartTag()
|
||||
let endPending: Token.EndTag = Token.EndTag()
|
||||
let charPending: Token.Char = Token.Char()
|
||||
let doctypePending: Token.Doctype = Token.Doctype() // doctype building up
|
||||
let commentPending: Token.Comment = Token.Comment() // comment building up
|
||||
private var lastStartTag: String? // the last start tag emitted, to test appropriate end tag
|
||||
private var selfClosingFlagAcknowledged: Bool = true
|
||||
|
||||
init(_ reader: CharacterReader, _ errors: ParseErrorList?) {
|
||||
self.reader = reader
|
||||
self.errors = errors
|
||||
}
|
||||
|
||||
func read()throws->Token {
|
||||
if (!selfClosingFlagAcknowledged) {
|
||||
error("Self closing flag not acknowledged")
|
||||
selfClosingFlagAcknowledged = true
|
||||
}
|
||||
|
||||
while (!isEmitPending) {
|
||||
try state.read(self, reader)
|
||||
}
|
||||
|
||||
// if emit is pending, a non-character token was found: return any chars in buffer, and leave token for next read:
|
||||
if (charsBuilder.length > 0) {
|
||||
let str: String = charsBuilder.toString()
|
||||
charsBuilder.clear()
|
||||
charsString = nil
|
||||
return charPending.data(str)
|
||||
} else if (charsString != nil) {
|
||||
let token: Token = charPending.data(charsString!)
|
||||
charsString = nil
|
||||
return token
|
||||
} else {
|
||||
isEmitPending = false
|
||||
return emitPending!
|
||||
}
|
||||
}
|
||||
|
||||
func emit(_ token: Token)throws {
|
||||
try Validate.isFalse(val: isEmitPending, msg: "There is an unread token pending!")
|
||||
|
||||
emitPending = token
|
||||
isEmitPending = true
|
||||
|
||||
if (token.type == Token.TokenType.StartTag) {
|
||||
let startTag: Token.StartTag = token as! Token.StartTag
|
||||
lastStartTag = startTag._tagName!
|
||||
if (startTag._selfClosing) {
|
||||
selfClosingFlagAcknowledged = false
|
||||
}
|
||||
} else if (token.type == Token.TokenType.EndTag) {
|
||||
let endTag: Token.EndTag = token as! Token.EndTag
|
||||
if (endTag._attributes.size() != 0) {
|
||||
error("Attributes incorrectly present on end tag")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func emit(_ str: String ) {
|
||||
// buffer strings up until last string token found, to emit only one token for a run of character refs etc.
|
||||
// does not set isEmitPending; read checks that
|
||||
if (charsString == nil) {
|
||||
charsString = str
|
||||
} else {
|
||||
if (charsBuilder.length == 0) { // switching to string builder as more than one emit before read
|
||||
charsBuilder.append(charsString!)
|
||||
}
|
||||
charsBuilder.append(str)
|
||||
}
|
||||
}
|
||||
|
||||
func emit(_ chars: [UnicodeScalar]) {
|
||||
emit(String(chars.map {Character($0)}))
|
||||
}
|
||||
|
||||
// func emit(_ codepoints: [Int]) {
|
||||
// emit(String(codepoints, 0, codepoints.length));
|
||||
// }
|
||||
|
||||
func emit(_ c: UnicodeScalar) {
|
||||
emit(String(c))
|
||||
}
|
||||
|
||||
func getState() -> TokeniserState {
|
||||
return state
|
||||
}
|
||||
|
||||
func transition(_ state: TokeniserState) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
func advanceTransition(_ state: TokeniserState) {
|
||||
reader.advance()
|
||||
self.state = state
|
||||
}
|
||||
|
||||
func acknowledgeSelfClosingFlag() {
|
||||
selfClosingFlagAcknowledged = true
|
||||
}
|
||||
|
||||
private var codepointHolder: [UnicodeScalar] = [UnicodeScalar(0)!] // holder to not have to keep creating arrays
|
||||
private var multipointHolder: [UnicodeScalar] = [UnicodeScalar(0)!, UnicodeScalar(0)!]
|
||||
|
||||
func consumeCharacterReference(_ additionalAllowedCharacter: UnicodeScalar?, _ inAttribute: Bool)throws->[UnicodeScalar]? {
|
||||
if (reader.isEmpty()) {
|
||||
return nil
|
||||
}
|
||||
if (additionalAllowedCharacter != nil && additionalAllowedCharacter == reader.current()) {
|
||||
return nil
|
||||
}
|
||||
if (reader.matchesAnySorted(Tokeniser.notCharRefCharsSorted)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var codeRef: [UnicodeScalar] = codepointHolder
|
||||
reader.markPos()
|
||||
if (reader.matchConsume("#")) { // numbered
|
||||
let isHexMode: Bool = reader.matchConsumeIgnoreCase("X")
|
||||
let numRef: String = isHexMode ? reader.consumeHexSequence() : reader.consumeDigitSequence()
|
||||
if (numRef.unicodeScalars.count == 0) { // didn't match anything
|
||||
characterReferenceError("numeric reference with no numerals")
|
||||
reader.rewindToMark()
|
||||
return nil
|
||||
}
|
||||
if (!reader.matchConsume(";")) {
|
||||
characterReferenceError("missing semicolon") // missing semi
|
||||
}
|
||||
var charval: Int = -1
|
||||
|
||||
let base: Int = isHexMode ? 16 : 10
|
||||
if let num = Int(numRef, radix: base) {
|
||||
charval = num
|
||||
}
|
||||
|
||||
if (charval == -1 || (charval >= 0xD800 && charval <= 0xDFFF) || charval > 0x10FFFF) {
|
||||
characterReferenceError("character outside of valid range")
|
||||
codeRef[0] = Tokeniser.replacementChar
|
||||
return codeRef
|
||||
} else {
|
||||
// todo: implement number replacement table
|
||||
// todo: check for extra illegal unicode points as parse errors
|
||||
codeRef[0] = UnicodeScalar(charval)!
|
||||
return codeRef
|
||||
}
|
||||
} else { // named
|
||||
// get as many letters as possible, and look for matching entities.
|
||||
let nameRef: String = reader.consumeLetterThenDigitSequence()
|
||||
let looksLegit: Bool = reader.matches(";")
|
||||
// found if a base named entity without a ;, or an extended entity with the ;.
|
||||
let found: Bool = (Entities.isBaseNamedEntity(nameRef) || (Entities.isNamedEntity(nameRef) && looksLegit))
|
||||
|
||||
if (!found) {
|
||||
reader.rewindToMark()
|
||||
if (looksLegit) { // named with semicolon
|
||||
characterReferenceError("invalid named referenece '\(nameRef)'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if (inAttribute && (reader.matchesLetter() || reader.matchesDigit() || reader.matchesAny("=", "-", "_"))) {
|
||||
// don't want that to match
|
||||
reader.rewindToMark()
|
||||
return nil
|
||||
}
|
||||
if (!reader.matchConsume(";")) {
|
||||
characterReferenceError("missing semicolon") // missing semi
|
||||
}
|
||||
let numChars: Int = Entities.codepointsForName(nameRef, codepoints: &multipointHolder)
|
||||
if (numChars == 1) {
|
||||
codeRef[0] = multipointHolder[0]
|
||||
return codeRef
|
||||
} else if (numChars == 2) {
|
||||
return multipointHolder
|
||||
} else {
|
||||
try Validate.fail(msg: "Unexpected characters returned for \(nameRef) num: \(numChars)")
|
||||
return multipointHolder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func createTagPending(_ start: Bool)->Token.Tag {
|
||||
tagPending = start ? startPending.reset() : endPending.reset()
|
||||
return tagPending
|
||||
}
|
||||
|
||||
func emitTagPending()throws {
|
||||
try tagPending.finaliseTag()
|
||||
try emit(tagPending)
|
||||
}
|
||||
|
||||
func createCommentPending() {
|
||||
commentPending.reset()
|
||||
}
|
||||
|
||||
func emitCommentPending()throws {
|
||||
try emit(commentPending)
|
||||
}
|
||||
|
||||
func createDoctypePending() {
|
||||
doctypePending.reset()
|
||||
}
|
||||
|
||||
func emitDoctypePending()throws {
|
||||
try emit(doctypePending)
|
||||
}
|
||||
|
||||
func createTempBuffer() {
|
||||
Token.reset(dataBuffer)
|
||||
}
|
||||
|
||||
func isAppropriateEndTagToken()throws->Bool {
|
||||
if(lastStartTag != nil) {
|
||||
let s = try tagPending.name()
|
||||
return s.equalsIgnoreCase(string: lastStartTag!)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appropriateEndTagName() -> String? {
|
||||
if (lastStartTag == nil) {
|
||||
return nil
|
||||
}
|
||||
return lastStartTag
|
||||
}
|
||||
|
||||
func error(_ state: TokeniserState) {
|
||||
if (errors != nil && errors!.canAddError()) {
|
||||
errors?.add(ParseError(reader.getPos(), "Unexpected character '\(String(reader.current()))' in input state [\(state.description)]"))
|
||||
}
|
||||
}
|
||||
|
||||
func eofError(_ state: TokeniserState) {
|
||||
if (errors != nil && errors!.canAddError()) {
|
||||
errors?.add(ParseError(reader.getPos(), "Unexpectedly reached end of file (EOF) in input state [\(state.description)]"))
|
||||
}
|
||||
}
|
||||
|
||||
private func characterReferenceError(_ message: String) {
|
||||
if (errors != nil && errors!.canAddError()) {
|
||||
errors?.add(ParseError(reader.getPos(), "Invalid character reference: \(message)"))
|
||||
}
|
||||
}
|
||||
|
||||
private func error(_ errorMsg: String) {
|
||||
if (errors != nil && errors!.canAddError()) {
|
||||
errors?.add(ParseError(reader.getPos(), errorMsg))
|
||||
}
|
||||
}
|
||||
|
||||
func currentNodeInHtmlNS() -> Bool {
|
||||
// todo: implement namespaces correctly
|
||||
return true
|
||||
// Element currentNode = currentNode()
|
||||
// return currentNode != null && currentNode.namespace().equals("HTML")
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to consume reader and unescape entities found within.
|
||||
* @param inAttribute
|
||||
* @return unescaped string from reader
|
||||
*/
|
||||
func unescapeEntities(_ inAttribute: Bool)throws->String {
|
||||
let builder: StringBuilder = StringBuilder()
|
||||
while (!reader.isEmpty()) {
|
||||
builder.append(reader.consumeTo(UnicodeScalar.Ampersand))
|
||||
if (reader.matches(UnicodeScalar.Ampersand)) {
|
||||
reader.consume()
|
||||
if let c = try consumeCharacterReference(nil, inAttribute) {
|
||||
if (c.count==0) {
|
||||
builder.append(UnicodeScalar.Ampersand)
|
||||
} else {
|
||||
builder.appendCodePoint(c[0])
|
||||
if (c.count == 2) {
|
||||
builder.appendCodePoint(c[1])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
builder.append(UnicodeScalar.Ampersand)
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
}
|
||||
+1644
File diff suppressed because it is too large
Load Diff
Generated
+98
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// TreeBuilder.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 24/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class TreeBuilder {
|
||||
public var reader: CharacterReader
|
||||
var tokeniser: Tokeniser
|
||||
public var doc: Document // current doc we are building into
|
||||
public var stack: Array<Element> // the stack of open elements
|
||||
public var baseUri: String // current base uri, for creating new elements
|
||||
public var currentToken: Token? // currentToken is used only for error tracking.
|
||||
public var errors: ParseErrorList // null when not tracking errors
|
||||
public var settings: ParseSettings
|
||||
|
||||
private let start: Token.StartTag = Token.StartTag() // start tag to process
|
||||
private let end: Token.EndTag = Token.EndTag()
|
||||
|
||||
public func defaultSettings() -> ParseSettings {preconditionFailure("This method must be overridden")}
|
||||
|
||||
public init() {
|
||||
doc = Document("")
|
||||
reader = CharacterReader("")
|
||||
tokeniser = Tokeniser(reader, nil)
|
||||
stack = Array<Element>()
|
||||
baseUri = ""
|
||||
errors = ParseErrorList(0, 0)
|
||||
settings = ParseSettings(false, false)
|
||||
}
|
||||
|
||||
public func initialiseParse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings) {
|
||||
doc = Document(baseUri)
|
||||
self.settings = settings
|
||||
reader = CharacterReader(input)
|
||||
self.errors = errors
|
||||
tokeniser = Tokeniser(reader, errors)
|
||||
stack = Array<Element>()
|
||||
self.baseUri = baseUri
|
||||
}
|
||||
|
||||
func parse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings)throws->Document {
|
||||
initialiseParse(input, baseUri, errors, settings)
|
||||
try runParser()
|
||||
return doc
|
||||
}
|
||||
|
||||
public func runParser()throws {
|
||||
while (true) {
|
||||
let token: Token = try tokeniser.read()
|
||||
try process(token)
|
||||
token.reset()
|
||||
|
||||
if (token.type == Token.TokenType.EOF) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func process(_ token: Token)throws->Bool {preconditionFailure("This method must be overridden")}
|
||||
|
||||
@discardableResult
|
||||
public func processStartTag(_ name: String)throws->Bool {
|
||||
if (currentToken === start) { // don't recycle an in-use token
|
||||
return try process(Token.StartTag().name(name))
|
||||
}
|
||||
return try process(start.reset().name(name))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func processStartTag(_ name: String, _ attrs: Attributes)throws->Bool {
|
||||
if (currentToken === start) { // don't recycle an in-use token
|
||||
return try process(Token.StartTag().nameAttr(name, attrs))
|
||||
}
|
||||
start.reset()
|
||||
start.nameAttr(name, attrs)
|
||||
return try process(start)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func processEndTag(_ name: String)throws->Bool {
|
||||
if (currentToken === end) { // don't recycle an in-use token
|
||||
return try process(Token.EndTag().name(name))
|
||||
}
|
||||
|
||||
return try process(end.reset().name(name))
|
||||
}
|
||||
|
||||
public func currentElement() -> Element? {
|
||||
let size: Int = stack.count
|
||||
return size > 0 ? stack[size-1] : nil
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// UnicodeScalar.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 14/11/16.
|
||||
// Copyright © 2016 Nabil Chatbi. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private let uppercaseSet = CharacterSet.uppercaseLetters
|
||||
private let lowercaseSet = CharacterSet.lowercaseLetters
|
||||
private let alphaSet = CharacterSet.letters
|
||||
private let alphaNumericSet = CharacterSet.alphanumerics
|
||||
private let symbolSet = CharacterSet.symbols
|
||||
private let digitSet = CharacterSet.decimalDigits
|
||||
|
||||
extension UnicodeScalar {
|
||||
public static let Ampersand: UnicodeScalar = "&"
|
||||
public static let LessThan: UnicodeScalar = "<"
|
||||
public static let GreaterThan: UnicodeScalar = ">"
|
||||
|
||||
public static let Space: UnicodeScalar = " "
|
||||
public static let BackslashF: UnicodeScalar = UnicodeScalar(12)
|
||||
public static let BackslashT: UnicodeScalar = "\t"
|
||||
public static let BackslashN: UnicodeScalar = "\n"
|
||||
public static let BackslashR: UnicodeScalar = "\r"
|
||||
public static let Slash: UnicodeScalar = "/"
|
||||
|
||||
public static let FormFeed: UnicodeScalar = "\u{000B}"// Form Feed
|
||||
public static let VerticalTab: UnicodeScalar = "\u{000C}"// vertical tab
|
||||
|
||||
func isMemberOfCharacterSet(_ set: CharacterSet) -> Bool {
|
||||
return set.contains(self)
|
||||
}
|
||||
|
||||
/// True for any space character, and the control characters \t, \n, \r, \f, \v.
|
||||
var isWhitespace: Bool {
|
||||
|
||||
switch self {
|
||||
|
||||
case UnicodeScalar.Space, UnicodeScalar.BackslashT, UnicodeScalar.BackslashN, UnicodeScalar.BackslashR, UnicodeScalar.BackslashF: return true
|
||||
|
||||
case UnicodeScalar.FormFeed, UnicodeScalar.VerticalTab: return true // Form Feed, vertical tab
|
||||
|
||||
default: return false
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// `true` if `self` normalized contains a single code unit that is in the categories of Uppercase and Titlecase Letters.
|
||||
var isUppercase: Bool {
|
||||
return isMemberOfCharacterSet(uppercaseSet)
|
||||
}
|
||||
|
||||
/// `true` if `self` normalized contains a single code unit that is in the category of Lowercase Letters.
|
||||
var isLowercase: Bool {
|
||||
return isMemberOfCharacterSet(lowercaseSet)
|
||||
|
||||
}
|
||||
|
||||
var uppercase: UnicodeScalar {
|
||||
let str = String(self).uppercased()
|
||||
return str.unicodeScalar(0)
|
||||
}
|
||||
}
|
||||
Generated
+133
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// Validate.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 02/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Validate {
|
||||
|
||||
/**
|
||||
* Validates that the object is not null
|
||||
* @param obj object to test
|
||||
*/
|
||||
public static func notNull(obj: Any?) throws {
|
||||
if (obj == nil) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Object must not be null")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the object is not null
|
||||
* @param obj object to test
|
||||
* @param msg message to output if validation fails
|
||||
*/
|
||||
public static func notNull(obj: AnyObject?, msg: String) throws {
|
||||
if (obj == nil) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the value is true
|
||||
* @param val object to test
|
||||
*/
|
||||
public static func isTrue(val: Bool) throws {
|
||||
if (!val) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Must be true")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the value is true
|
||||
* @param val object to test
|
||||
* @param msg message to output if validation fails
|
||||
*/
|
||||
public static func isTrue(val: Bool, msg: String) throws {
|
||||
if (!val) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the value is false
|
||||
* @param val object to test
|
||||
*/
|
||||
public static func isFalse(val: Bool) throws {
|
||||
if (val) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Must be false")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the value is false
|
||||
* @param val object to test
|
||||
* @param msg message to output if validation fails
|
||||
*/
|
||||
public static func isFalse(val: Bool, msg: String) throws {
|
||||
if (val) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the array contains no null elements
|
||||
* @param objects the array to test
|
||||
*/
|
||||
public static func noNullElements(objects: [AnyObject?]) throws {
|
||||
try noNullElements(objects: objects, msg: "Array must not contain any null objects")
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the array contains no null elements
|
||||
* @param objects the array to test
|
||||
* @param msg message to output if validation fails
|
||||
*/
|
||||
public static func noNullElements(objects: [AnyObject?], msg: String) throws {
|
||||
for obj in objects {
|
||||
if (obj == nil) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the string is not empty
|
||||
* @param string the string to test
|
||||
*/
|
||||
public static func notEmpty(string: String?) throws {
|
||||
if (string == nil || string?.count == 0) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "String must not be empty")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the string is not empty
|
||||
* @param string the string to test
|
||||
* @param msg message to output if validation fails
|
||||
*/
|
||||
public static func notEmpty(string: String?, msg: String ) throws {
|
||||
if (string == nil || string?.count == 0) {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Cause a failure.
|
||||
@param msg message to output.
|
||||
*/
|
||||
public static func fail(msg: String) throws {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg)
|
||||
}
|
||||
|
||||
/**
|
||||
Helper
|
||||
*/
|
||||
public static func exception(msg: String) throws {
|
||||
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: msg)
|
||||
}
|
||||
}
|
||||
Generated
+650
@@ -0,0 +1,650 @@
|
||||
//
|
||||
// Whitelist.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 14/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
/*
|
||||
Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired
|
||||
this whitelist configuration, and the initial defaults.
|
||||
*/
|
||||
|
||||
/**
|
||||
Whitelists define what HTML (elements and attributes) to allow through the cleaner. Everything else is removed.
|
||||
<p>
|
||||
Start with one of the defaults:
|
||||
</p>
|
||||
<ul>
|
||||
<li>{@link #none}
|
||||
<li>{@link #simpleText}
|
||||
<li>{@link #basic}
|
||||
<li>{@link #basicWithImages}
|
||||
<li>{@link #relaxed}
|
||||
</ul>
|
||||
<p>
|
||||
If you need to allow more through (please be careful!), tweak a base whitelist with:
|
||||
</p>
|
||||
<ul>
|
||||
<li>{@link #addTags}
|
||||
<li>{@link #addAttributes}
|
||||
<li>{@link #addEnforcedAttribute}
|
||||
<li>{@link #addProtocols}
|
||||
</ul>
|
||||
<p>
|
||||
You can remove any setting from an existing whitelist with:
|
||||
</p>
|
||||
<ul>
|
||||
<li>{@link #removeTags}
|
||||
<li>{@link #removeAttributes}
|
||||
<li>{@link #removeEnforcedAttribute}
|
||||
<li>{@link #removeProtocols}
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
The cleaner and these whitelists assume that you want to clean a <code>body</code> fragment of HTML (to add user
|
||||
supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, either wrap the
|
||||
document HTML around the cleaned body HTML, or create a whitelist that allows <code>html</code> and <code>head</code>
|
||||
elements as appropriate.
|
||||
</p>
|
||||
<p>
|
||||
If you are going to extend a whitelist, please be very careful. Make sure you understand what attributes may lead to
|
||||
XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See
|
||||
http://ha.ckers.org/xss.html for some XSS attack examples.
|
||||
</p>
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public class Whitelist {
|
||||
private var tagNames: Set<TagName> // tags allowed, lower case. e.g. [p, br, span]
|
||||
private var attributes: Dictionary<TagName, Set<AttributeKey>> // tag -> attribute[]. allowed attributes [href] for a tag.
|
||||
private var enforcedAttributes: Dictionary<TagName, Dictionary<AttributeKey, AttributeValue>> // always set these attribute values
|
||||
private var protocols: Dictionary<TagName, Dictionary<AttributeKey, Set<Protocol>>> // allowed URL protocols for attributes
|
||||
private var preserveRelativeLinks: Bool // option to preserve relative links
|
||||
|
||||
/**
|
||||
This whitelist allows only text nodes: all HTML will be stripped.
|
||||
|
||||
@return whitelist
|
||||
*/
|
||||
public static func none() -> Whitelist {
|
||||
return Whitelist()
|
||||
}
|
||||
|
||||
/**
|
||||
This whitelist allows only simple text formatting: <code>b, em, i, strong, u</code>. All other HTML (tags and
|
||||
attributes) will be removed.
|
||||
|
||||
@return whitelist
|
||||
*/
|
||||
public static func simpleText()throws ->Whitelist {
|
||||
return try Whitelist().addTags("b", "em", "i", "strong", "u")
|
||||
}
|
||||
|
||||
/**
|
||||
<p>
|
||||
This whitelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li,
|
||||
ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul</code>, and appropriate attributes.
|
||||
</p>
|
||||
<p>
|
||||
Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced
|
||||
<code>rel=nofollow</code> attribute.
|
||||
</p>
|
||||
<p>
|
||||
Does not allow images.
|
||||
</p>
|
||||
|
||||
@return whitelist
|
||||
*/
|
||||
public static func basic()throws->Whitelist {
|
||||
return try Whitelist()
|
||||
.addTags(
|
||||
"a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em",
|
||||
"i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub",
|
||||
"sup", "u", "ul")
|
||||
|
||||
.addAttributes("a", "href")
|
||||
.addAttributes("blockquote", "cite")
|
||||
.addAttributes("q", "cite")
|
||||
|
||||
.addProtocols("a", "href", "ftp", "http", "https", "mailto")
|
||||
.addProtocols("blockquote", "cite", "http", "https")
|
||||
.addProtocols("cite", "cite", "http", "https")
|
||||
|
||||
.addEnforcedAttribute("a", "rel", "nofollow")
|
||||
}
|
||||
|
||||
/**
|
||||
This whitelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate
|
||||
attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>.
|
||||
|
||||
@return whitelist
|
||||
*/
|
||||
public static func basicWithImages()throws->Whitelist {
|
||||
return try basic()
|
||||
.addTags("img")
|
||||
.addAttributes("img", "align", "alt", "height", "src", "title", "width")
|
||||
.addProtocols("img", "src", "http", "https")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
This whitelist allows a full range of text and structural body HTML: <code>a, b, blockquote, br, caption, cite,
|
||||
code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub,
|
||||
sup, table, tbody, td, tfoot, th, thead, tr, u, ul</code>
|
||||
<p>
|
||||
Links do not have an enforced <code>rel=nofollow</code> attribute, but you can add that if desired.
|
||||
</p>
|
||||
|
||||
@return whitelist
|
||||
*/
|
||||
public static func relaxed()throws->Whitelist {
|
||||
return try Whitelist()
|
||||
.addTags(
|
||||
"a", "b", "blockquote", "br", "caption", "cite", "code", "col",
|
||||
"colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong",
|
||||
"sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
|
||||
"ul")
|
||||
|
||||
.addAttributes("a", "href", "title")
|
||||
.addAttributes("blockquote", "cite")
|
||||
.addAttributes("col", "span", "width")
|
||||
.addAttributes("colgroup", "span", "width")
|
||||
.addAttributes("img", "align", "alt", "height", "src", "title", "width")
|
||||
.addAttributes("ol", "start", "type")
|
||||
.addAttributes("q", "cite")
|
||||
.addAttributes("table", "summary", "width")
|
||||
.addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
|
||||
.addAttributes(
|
||||
"th", "abbr", "axis", "colspan", "rowspan", "scope",
|
||||
"width")
|
||||
.addAttributes("ul", "type")
|
||||
|
||||
.addProtocols("a", "href", "ftp", "http", "https", "mailto")
|
||||
.addProtocols("blockquote", "cite", "http", "https")
|
||||
.addProtocols("cite", "cite", "http", "https")
|
||||
.addProtocols("img", "src", "http", "https")
|
||||
.addProtocols("q", "cite", "http", "https")
|
||||
}
|
||||
|
||||
/**
|
||||
Create a new, empty whitelist. Generally it will be better to start with a default prepared whitelist instead.
|
||||
|
||||
@see #basic()
|
||||
@see #basicWithImages()
|
||||
@see #simpleText()
|
||||
@see #relaxed()
|
||||
*/
|
||||
init() {
|
||||
tagNames = Set<TagName>()
|
||||
attributes = Dictionary<TagName, Set<AttributeKey>>()
|
||||
enforcedAttributes = Dictionary<TagName, Dictionary<AttributeKey, AttributeValue>>()
|
||||
protocols = Dictionary<TagName, Dictionary<AttributeKey, Set<Protocol>>>()
|
||||
preserveRelativeLinks = false
|
||||
}
|
||||
|
||||
/**
|
||||
Add a list of allowed elements to a whitelist. (If a tag is not allowed, it will be removed from the HTML.)
|
||||
|
||||
@param tags tag names to allow
|
||||
@return this (for chaining)
|
||||
*/
|
||||
@discardableResult
|
||||
open func addTags(_ tags: String...)throws ->Whitelist {
|
||||
for tagName in tags {
|
||||
try Validate.notEmpty(string: tagName)
|
||||
tagNames.insert(TagName.valueOf(tagName))
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a list of allowed elements from a whitelist. (If a tag is not allowed, it will be removed from the HTML.)
|
||||
|
||||
@param tags tag names to disallow
|
||||
@return this (for chaining)
|
||||
*/
|
||||
@discardableResult
|
||||
open func removeTags(_ tags: String...)throws ->Whitelist {
|
||||
try Validate.notNull(obj: tags)
|
||||
|
||||
for tag in tags {
|
||||
try Validate.notEmpty(string: tag)
|
||||
let tagName: TagName = TagName.valueOf(tag)
|
||||
|
||||
if(tagNames.contains(tagName)) { // Only look in sub-maps if tag was allowed
|
||||
tagNames.remove(tagName)
|
||||
attributes.removeValue(forKey: tagName)
|
||||
enforcedAttributes.removeValue(forKey: tagName)
|
||||
protocols.removeValue(forKey: tagName)
|
||||
}
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.)
|
||||
<p>
|
||||
E.g.: <code>addAttributes("a", "href", "class")</code> allows <code>href</code> and <code>class</code> attributes
|
||||
on <code>a</code> tags.
|
||||
</p>
|
||||
<p>
|
||||
To make an attribute valid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
|
||||
<code>addAttributes(":all", "class")</code>.
|
||||
</p>
|
||||
|
||||
@param tag The tag the attributes are for. The tag will be added to the allowed tag list if necessary.
|
||||
@param keys List of valid attributes for the tag
|
||||
@return this (for chaining)
|
||||
*/
|
||||
@discardableResult
|
||||
open func addAttributes(_ tag: String, _ keys: String...)throws->Whitelist {
|
||||
try Validate.notEmpty(string: tag)
|
||||
try Validate.isTrue(val: keys.count > 0, msg: "No attributes supplied.")
|
||||
|
||||
let tagName = TagName.valueOf(tag)
|
||||
if (!tagNames.contains(tagName)) {
|
||||
tagNames.insert(tagName)
|
||||
}
|
||||
var attributeSet = Set<AttributeKey>()
|
||||
for key in keys {
|
||||
try Validate.notEmpty(string: key)
|
||||
attributeSet.insert(AttributeKey.valueOf(key))
|
||||
}
|
||||
|
||||
if var currentSet = attributes[tagName] {
|
||||
for at in attributeSet {
|
||||
currentSet.insert(at)
|
||||
}
|
||||
attributes[tagName] = currentSet
|
||||
} else {
|
||||
attributes[tagName] = attributeSet
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a list of allowed attributes from a tag. (If an attribute is not allowed on an element, it will be removed.)
|
||||
<p>
|
||||
E.g.: <code>removeAttributes("a", "href", "class")</code> disallows <code>href</code> and <code>class</code>
|
||||
attributes on <code>a</code> tags.
|
||||
</p>
|
||||
<p>
|
||||
To make an attribute invalid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
|
||||
<code>removeAttributes(":all", "class")</code>.
|
||||
</p>
|
||||
|
||||
@param tag The tag the attributes are for.
|
||||
@param keys List of invalid attributes for the tag
|
||||
@return this (for chaining)
|
||||
*/
|
||||
@discardableResult
|
||||
open func removeAttributes(_ tag: String, _ keys: String...)throws->Whitelist {
|
||||
try Validate.notEmpty(string: tag)
|
||||
try Validate.isTrue(val: keys.count > 0, msg: "No attributes supplied.")
|
||||
|
||||
let tagName: TagName = TagName.valueOf(tag)
|
||||
var attributeSet = Set<AttributeKey>()
|
||||
for key in keys {
|
||||
try Validate.notEmpty(string: key)
|
||||
attributeSet.insert(AttributeKey.valueOf(key))
|
||||
}
|
||||
|
||||
if(tagNames.contains(tagName)) { // Only look in sub-maps if tag was allowed
|
||||
if var currentSet = attributes[tagName] {
|
||||
for l in attributeSet {
|
||||
currentSet.remove(l)
|
||||
}
|
||||
attributes[tagName] = currentSet
|
||||
if(currentSet.isEmpty) { // Remove tag from attribute map if no attributes are allowed for tag
|
||||
attributes.removeValue(forKey: tagName)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(tag == ":all") { // Attribute needs to be removed from all individually set tags
|
||||
for name in attributes.keys {
|
||||
var currentSet: Set<AttributeKey> = attributes[name]!
|
||||
for l in attributeSet {
|
||||
currentSet.remove(l)
|
||||
}
|
||||
attributes[name] = currentSet
|
||||
if(currentSet.isEmpty) { // Remove tag from attribute map if no attributes are allowed for tag
|
||||
attributes.removeValue(forKey: name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element
|
||||
already has the attribute set, it will be overridden.
|
||||
<p>
|
||||
E.g.: <code>addEnforcedAttribute("a", "rel", "nofollow")</code> will make all <code>a</code> tags output as
|
||||
<code><a href="..." rel="nofollow"></code>
|
||||
</p>
|
||||
|
||||
@param tag The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary.
|
||||
@param key The attribute key
|
||||
@param value The enforced attribute value
|
||||
@return this (for chaining)
|
||||
*/
|
||||
@discardableResult
|
||||
open func addEnforcedAttribute(_ tag: String, _ key: String, _ value: String)throws->Whitelist {
|
||||
try Validate.notEmpty(string: tag)
|
||||
try Validate.notEmpty(string: key)
|
||||
try Validate.notEmpty(string: value)
|
||||
|
||||
let tagName: TagName = TagName.valueOf(tag)
|
||||
if (!tagNames.contains(tagName)) {
|
||||
tagNames.insert(tagName)
|
||||
}
|
||||
let attrKey: AttributeKey = AttributeKey.valueOf(key)
|
||||
let attrVal: AttributeValue = AttributeValue.valueOf(value)
|
||||
|
||||
if (enforcedAttributes[tagName] != nil) {
|
||||
enforcedAttributes[tagName]?[attrKey] = attrVal
|
||||
} else {
|
||||
var attrMap: Dictionary<AttributeKey, AttributeValue> = Dictionary<AttributeKey, AttributeValue>()
|
||||
attrMap[attrKey] = attrVal
|
||||
enforcedAttributes[tagName] = attrMap
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a previously configured enforced attribute from a tag.
|
||||
|
||||
@param tag The tag the enforced attribute is for.
|
||||
@param key The attribute key
|
||||
@return this (for chaining)
|
||||
*/
|
||||
@discardableResult
|
||||
open func removeEnforcedAttribute(_ tag: String, _ key: String)throws->Whitelist {
|
||||
try Validate.notEmpty(string: tag)
|
||||
try Validate.notEmpty(string: key)
|
||||
|
||||
let tagName: TagName = TagName.valueOf(tag)
|
||||
if(tagNames.contains(tagName) && (enforcedAttributes[tagName] != nil)) {
|
||||
let attrKey: AttributeKey = AttributeKey.valueOf(key)
|
||||
var attrMap: Dictionary<AttributeKey, AttributeValue> = enforcedAttributes[tagName]!
|
||||
attrMap.removeValue(forKey: attrKey)
|
||||
enforcedAttributes[tagName] = attrMap
|
||||
|
||||
if(attrMap.isEmpty) { // Remove tag from enforced attribute map if no enforced attributes are present
|
||||
enforcedAttributes.removeValue(forKey: tagName)
|
||||
}
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure this Whitelist to preserve relative links in an element's URL attribute, or convert them to absolute
|
||||
* links. By default, this is <b>false</b>: URLs will be made absolute (e.g. start with an allowed protocol, like
|
||||
* e.g. {@code http://}.
|
||||
* <p>
|
||||
* Note that when handling relative links, the input document must have an appropriate {@code base URI} set when
|
||||
* parsing, so that the link's protocol can be confirmed. Regardless of the setting of the {@code preserve relative
|
||||
* links} option, the link must be resolvable against the base URI to an allowed protocol; otherwise the attribute
|
||||
* will be removed.
|
||||
* </p>
|
||||
*
|
||||
* @param preserve {@code true} to allow relative links, {@code false} (default) to deny
|
||||
* @return this Whitelist, for chaining.
|
||||
* @see #addProtocols
|
||||
*/
|
||||
@discardableResult
|
||||
open func preserveRelativeLinks(_ preserve: Bool) -> Whitelist {
|
||||
preserveRelativeLinks = preserve
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to
|
||||
URLs with the defined protocol.
|
||||
<p>
|
||||
E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code>
|
||||
</p>
|
||||
<p>
|
||||
To allow a link to an in-page URL anchor (i.e. <code><a href="#anchor"></code>, add a <code>#</code>:<br>
|
||||
E.g.: <code>addProtocols("a", "href", "#")</code>
|
||||
</p>
|
||||
|
||||
@param tag Tag the URL protocol is for
|
||||
@param key Attribute key
|
||||
@param protocols List of valid protocols
|
||||
@return this, for chaining
|
||||
*/
|
||||
@discardableResult
|
||||
open func addProtocols(_ tag: String, _ key: String, _ protocols: String...)throws->Whitelist {
|
||||
try Validate.notEmpty(string: tag)
|
||||
try Validate.notEmpty(string: key)
|
||||
|
||||
let tagName: TagName = TagName.valueOf(tag)
|
||||
let attrKey: AttributeKey = AttributeKey.valueOf(key)
|
||||
var attrMap: Dictionary<AttributeKey, Set<Protocol>>
|
||||
var protSet: Set<Protocol>
|
||||
|
||||
if (self.protocols[tagName] != nil) {
|
||||
attrMap = self.protocols[tagName]!
|
||||
} else {
|
||||
attrMap = Dictionary<AttributeKey, Set<Protocol>>()
|
||||
self.protocols[tagName] = attrMap
|
||||
}
|
||||
|
||||
if (attrMap[attrKey] != nil) {
|
||||
protSet = attrMap[attrKey]!
|
||||
} else {
|
||||
protSet = Set<Protocol>()
|
||||
attrMap[attrKey] = protSet
|
||||
self.protocols[tagName] = attrMap
|
||||
}
|
||||
for ptl in protocols {
|
||||
try Validate.notEmpty(string: ptl)
|
||||
let prot: Protocol = Protocol.valueOf(ptl)
|
||||
protSet.insert(prot)
|
||||
}
|
||||
attrMap[attrKey] = protSet
|
||||
self.protocols[tagName] = attrMap
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Remove allowed URL protocols for an element's URL attribute.
|
||||
<p>
|
||||
E.g.: <code>removeProtocols("a", "href", "ftp")</code>
|
||||
</p>
|
||||
|
||||
@param tag Tag the URL protocol is for
|
||||
@param key Attribute key
|
||||
@param protocols List of invalid protocols
|
||||
@return this, for chaining
|
||||
*/
|
||||
@discardableResult
|
||||
open func removeProtocols(_ tag: String, _ key: String, _ protocols: String...)throws->Whitelist {
|
||||
try Validate.notEmpty(string: tag)
|
||||
try Validate.notEmpty(string: key)
|
||||
|
||||
let tagName: TagName = TagName.valueOf(tag)
|
||||
let attrKey: AttributeKey = AttributeKey.valueOf(key)
|
||||
|
||||
if(self.protocols[tagName] != nil) {
|
||||
var attrMap: Dictionary<AttributeKey, Set<Protocol>> = self.protocols[tagName]!
|
||||
if(attrMap[attrKey] != nil) {
|
||||
var protSet: Set<Protocol> = attrMap[attrKey]!
|
||||
for ptl in protocols {
|
||||
try Validate.notEmpty(string: ptl)
|
||||
let prot: Protocol = Protocol.valueOf(ptl)
|
||||
protSet.remove(prot)
|
||||
}
|
||||
attrMap[attrKey] = protSet
|
||||
|
||||
if(protSet.isEmpty) { // Remove protocol set if empty
|
||||
attrMap.removeValue(forKey: attrKey)
|
||||
if(attrMap.isEmpty) { // Remove entry for tag if empty
|
||||
self.protocols.removeValue(forKey: tagName)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
self.protocols[tagName] = attrMap
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the supplied tag is allowed by this whitelist
|
||||
* @param tag test tag
|
||||
* @return true if allowed
|
||||
*/
|
||||
public func isSafeTag(_ tag: String) -> Bool {
|
||||
return tagNames.contains(TagName.valueOf(tag))
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the supplied attribute is allowed by this whitelist for this tag
|
||||
* @param tagName tag to consider allowing the attribute in
|
||||
* @param el element under test, to confirm protocol
|
||||
* @param attr attribute under test
|
||||
* @return true if allowed
|
||||
*/
|
||||
public func isSafeAttribute(_ tagName: String, _ el: Element, _ attr: Attribute)throws -> Bool {
|
||||
let tag: TagName = TagName.valueOf(tagName)
|
||||
let key: AttributeKey = AttributeKey.valueOf(attr.getKey())
|
||||
|
||||
if (attributes[tag] != nil) {
|
||||
if (attributes[tag]?.contains(key))! {
|
||||
if (protocols[tag] != nil) {
|
||||
let attrProts: Dictionary<AttributeKey, Set<Protocol>> = protocols[tag]!
|
||||
// ok if not defined protocol; otherwise test
|
||||
return try (attrProts[key] == nil) || testValidProtocol(el, attr, attrProts[key]!)
|
||||
} else { // attribute found, no protocols defined, so OK
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// no attributes defined for tag, try :all tag
|
||||
return try !(tagName == ":all") && isSafeAttribute(":all", el, attr)
|
||||
}
|
||||
|
||||
private func testValidProtocol(_ el: Element, _ attr: Attribute, _ protocols: Set<Protocol>)throws->Bool {
|
||||
// try to resolve relative urls to abs, and optionally update the attribute so output html has abs.
|
||||
// rels without a baseuri get removed
|
||||
var value: String = try el.absUrl(attr.getKey())
|
||||
if (value.count == 0) {
|
||||
value = attr.getValue()
|
||||
}// if it could not be made abs, run as-is to allow custom unknown protocols
|
||||
if (!preserveRelativeLinks) {
|
||||
attr.setValue(value: value)
|
||||
}
|
||||
|
||||
for ptl in protocols {
|
||||
var prot: String = ptl.toString()
|
||||
|
||||
if (prot=="#") { // allows anchor links
|
||||
if (isValidAnchor(value)) {
|
||||
return true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
prot += ":"
|
||||
|
||||
if (value.lowercased().hasPrefix(prot)) {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func isValidAnchor(_ value: String) -> Bool {
|
||||
return value.startsWith("#") && !(Pattern(".*\\s.*").matcher(in: value).count > 0)
|
||||
}
|
||||
|
||||
public func getEnforcedAttributes(_ tagName: String)throws->Attributes {
|
||||
let attrs: Attributes = Attributes()
|
||||
let tag: TagName = TagName.valueOf(tagName)
|
||||
if let keyVals: Dictionary<AttributeKey, AttributeValue> = enforcedAttributes[tag] {
|
||||
for entry in keyVals {
|
||||
try attrs.put(entry.key.toString(), entry.value.toString())
|
||||
}
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// named types for config. All just hold strings, but here for my sanity.
|
||||
|
||||
open class TagName: TypedValue {
|
||||
override init(_ value: String) {
|
||||
super.init(value)
|
||||
}
|
||||
|
||||
static func valueOf(_ value: String) -> TagName {
|
||||
return TagName(value)
|
||||
}
|
||||
}
|
||||
|
||||
open class AttributeKey: TypedValue {
|
||||
override init(_ value: String) {
|
||||
super.init(value)
|
||||
}
|
||||
|
||||
static func valueOf(_ value: String) -> AttributeKey {
|
||||
return AttributeKey(value)
|
||||
}
|
||||
}
|
||||
|
||||
open class AttributeValue: TypedValue {
|
||||
override init(_ value: String) {
|
||||
super.init(value)
|
||||
}
|
||||
|
||||
static func valueOf(_ value: String) -> AttributeValue {
|
||||
return AttributeValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
open class Protocol: TypedValue {
|
||||
override init(_ value: String) {
|
||||
super.init(value)
|
||||
}
|
||||
|
||||
static func valueOf(_ value: String) -> Protocol {
|
||||
return Protocol(value)
|
||||
}
|
||||
}
|
||||
|
||||
open class TypedValue {
|
||||
fileprivate let value: String
|
||||
|
||||
init(_ value: String) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public func toString() -> String {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
extension TypedValue: Hashable {
|
||||
public var hashValue: Int {
|
||||
return value.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
public func == (lhs: TypedValue, rhs: TypedValue) -> Bool {
|
||||
if(lhs === rhs) {return true}
|
||||
return lhs.value == rhs.value
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// XmlDeclaration.swift
|
||||
// SwifSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 29/09/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
An XML Declaration.
|
||||
*/
|
||||
public class XmlDeclaration: Node {
|
||||
private let _name: String
|
||||
private let isProcessingInstruction: Bool // <! if true, <? if false, declaration (and last data char should be ?)
|
||||
|
||||
/**
|
||||
Create a new XML declaration
|
||||
@param name of declaration
|
||||
@param baseUri base uri
|
||||
@param isProcessingInstruction is processing instruction
|
||||
*/
|
||||
public init(_ name: String, _ baseUri: String, _ isProcessingInstruction: Bool) {
|
||||
self._name = name
|
||||
self.isProcessingInstruction = isProcessingInstruction
|
||||
super.init(baseUri)
|
||||
}
|
||||
|
||||
public override func nodeName() -> String {
|
||||
return "#declaration"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of this declaration.
|
||||
* @return name of this declaration.
|
||||
*/
|
||||
public func name() -> String {
|
||||
return _name
|
||||
}
|
||||
|
||||
/**
|
||||
Get the unencoded XML declaration.
|
||||
@return XML declaration
|
||||
*/
|
||||
public func getWholeDeclaration()throws->String {
|
||||
return try attributes!.html().trim() // attr html starts with a " "
|
||||
}
|
||||
|
||||
override func outerHtmlHead(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {
|
||||
accum
|
||||
.append("<")
|
||||
.append(isProcessingInstruction ? "!" : "?")
|
||||
.append(_name)
|
||||
do {
|
||||
try attributes?.html(accum: accum, out: out)
|
||||
} catch {}
|
||||
accum
|
||||
.append(isProcessingInstruction ? "!" : "?")
|
||||
.append(">")
|
||||
}
|
||||
|
||||
override func outerHtmlTail(_ accum: StringBuilder, _ depth: Int, _ out: OutputSettings) {}
|
||||
|
||||
public override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let clone = XmlDeclaration(_name, baseUri!, isProcessingInstruction)
|
||||
return copy(clone: clone)
|
||||
}
|
||||
|
||||
public override func copy(parent: Node?) -> Node {
|
||||
let clone = XmlDeclaration(_name, baseUri!, isProcessingInstruction)
|
||||
return copy(clone: clone, parent: parent)
|
||||
}
|
||||
public override func copy(clone: Node, parent: Node?) -> Node {
|
||||
return super.copy(clone: clone, parent: parent)
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// XmlTreeBuilder.swift
|
||||
// SwiftSoup
|
||||
//
|
||||
// Created by Nabil Chatbi on 14/10/16.
|
||||
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Use the {@code XmlTreeBuilder} when you want to parse XML without any of the HTML DOM rules being applied to the
|
||||
* document.
|
||||
* <p>Usage example: {@code Document xmlDoc = Jsoup.parse(html, baseUrl, Parser.xmlParser())}</p>
|
||||
*
|
||||
*/
|
||||
public class XmlTreeBuilder: TreeBuilder {
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public override func defaultSettings() -> ParseSettings {
|
||||
return ParseSettings.preserveCase
|
||||
}
|
||||
|
||||
public func parse(_ input: String, _ baseUri: String)throws->Document {
|
||||
return try parse(input, baseUri, ParseErrorList.noTracking(), ParseSettings.preserveCase)
|
||||
}
|
||||
|
||||
override public func initialiseParse(_ input: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings) {
|
||||
super.initialiseParse(input, baseUri, errors, settings)
|
||||
stack.append(doc) // place the document onto the stack. differs from HtmlTreeBuilder (not on stack)
|
||||
doc.outputSettings().syntax(syntax: OutputSettings.Syntax.xml)
|
||||
}
|
||||
|
||||
override public func process(_ token: Token)throws->Bool {
|
||||
// start tag, end tag, doctype, comment, character, eof
|
||||
switch (token.type) {
|
||||
case .StartTag:
|
||||
try insert(token.asStartTag())
|
||||
break
|
||||
case .EndTag:
|
||||
try popStackToClose(token.asEndTag())
|
||||
break
|
||||
case .Comment:
|
||||
try insert(token.asComment())
|
||||
break
|
||||
case .Char:
|
||||
try insert(token.asCharacter())
|
||||
break
|
||||
case .Doctype:
|
||||
try insert(token.asDoctype())
|
||||
break
|
||||
case .EOF: // could put some normalisation here if desired
|
||||
break
|
||||
// default:
|
||||
// try Validate.fail(msg: "Unexpected token type: " + token.tokenType())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func insertNode(_ node: Node)throws {
|
||||
try currentElement()?.appendChild(node)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert(_ startTag: Token.StartTag)throws->Element {
|
||||
let tag: Tag = try Tag.valueOf(startTag.name(), settings)
|
||||
// todo: wonder if for xml parsing, should treat all tags as unknown? because it's not html.
|
||||
let el: Element = try Element(tag, baseUri, settings.normalizeAttributes(startTag._attributes))
|
||||
try insertNode(el)
|
||||
if (startTag.isSelfClosing()) {
|
||||
tokeniser.acknowledgeSelfClosingFlag()
|
||||
if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output. see above.
|
||||
{
|
||||
tag.setSelfClosing()
|
||||
}
|
||||
} else {
|
||||
stack.append(el)
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
func insert(_ commentToken: Token.Comment)throws {
|
||||
let comment: Comment = Comment(commentToken.getData(), baseUri)
|
||||
var insert: Node = comment
|
||||
if (commentToken.bogus) { // xml declarations are emitted as bogus comments (which is right for html, but not xml)
|
||||
// so we do a bit of a hack and parse the data as an element to pull the attributes out
|
||||
let data: String = comment.getData()
|
||||
if (data.count > 1 && (data.startsWith("!") || data.startsWith("?"))) {
|
||||
let doc: Document = try SwiftSoup.parse("<" + data.substring(1, data.count - 2) + ">", baseUri, Parser.xmlParser())
|
||||
let el: Element = doc.child(0)
|
||||
insert = XmlDeclaration(settings.normalizeTag(el.tagName()), comment.getBaseUri(), data.startsWith("!"))
|
||||
insert.getAttributes()?.addAll(incoming: el.getAttributes())
|
||||
}
|
||||
}
|
||||
try insertNode(insert)
|
||||
}
|
||||
|
||||
func insert(_ characterToken: Token.Char)throws {
|
||||
let node: Node = TextNode(characterToken.getData()!, baseUri)
|
||||
try insertNode(node)
|
||||
}
|
||||
|
||||
func insert(_ d: Token.Doctype)throws {
|
||||
let doctypeNode = DocumentType(settings.normalizeTag(d.getName()), d.getPubSysKey(), d.getPublicIdentifier(), d.getSystemIdentifier(), baseUri)
|
||||
try insertNode(doctypeNode)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the stack contains an element with this tag's name, pop up the stack to remove the first occurrence. If not
|
||||
* found, skips.
|
||||
*
|
||||
* @param endTag
|
||||
*/
|
||||
private func popStackToClose(_ endTag: Token.EndTag)throws {
|
||||
let elName: String = try endTag.name()
|
||||
var firstFound: Element? = nil
|
||||
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
if (next.nodeName().equals(elName)) {
|
||||
firstFound = next
|
||||
break
|
||||
}
|
||||
}
|
||||
if (firstFound == nil) {
|
||||
return // not found, skip
|
||||
}
|
||||
|
||||
for pos in (0..<stack.count).reversed() {
|
||||
let next: Element = stack[pos]
|
||||
stack.remove(at: pos)
|
||||
if (next == firstFound!) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseFragment(_ inputFragment: String, _ baseUri: String, _ errors: ParseErrorList, _ settings: ParseSettings)throws->Array<Node> {
|
||||
initialiseParse(inputFragment, baseUri, errors, settings)
|
||||
try runParser()
|
||||
return doc.getChildNodes()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user