A small menubar app that allows you to switch between R versions quickly (if you have multiple versions of R framework installed). https://rud.is/rswitch
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

322 lines
14 KiB

//
// 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 {
var sq = ""
while (!tq.isEmpty()) {
if (tq.matches("(")) {
sq.append("(")
sq.append(tq.chompBalanced("(", ")"))
sq.append(")")
} else if (tq.matches("[")) {
sq.append("[")
sq.append(tq.chompBalanced("[", "]"))
sq.append("]")
} else if (tq.matchesAny(QueryParser.combinators)) {
break
} else {
sq.append(tq.consume())
}
}
return sq
}
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)))
}
}