** Holds a document that may be queried with CSS selectors.
class SizzleDoc {
private static const Regex selectorRegex := theMainSelector
private NodeBucketMulti rootBucket
** Returns the root element of the XML document.
SElem root { private set }
** Create a 'SizzleDoc' from the given root SElem.
new make(SElem root) {
this.root = root
this.rootBucket = NodeBucketMulti(root, true)
}
** Create a 'SizzleDoc' from the given XML string.
static new fromXml(Str xml) {
SizzleXml(xml)->doc
}
private static Regex theMainSelector() {
Str nonWord1 := Str<| [^,#<+:\s\[\]\(\)\\\.] |>.trim
Str nonWord2 := Str<| [^,#<+:\s\[\]\(\)\\] |>.trim
Str typeSelector := Str<| (\w+)? |>.trim.replace("\\w", nonWord1)
Str idSelector := Str<| (?:#(\w+))? |>.trim.replace("\\w", nonWord1)
Str classesSelector := Str<| (\.[\w\.]+)? |>.trim.replace("\\w", nonWord2)
Str attrSelector := Str<| ((?:\[[^\]]+\])+)?(?:\s*([>+]))? |>.trim
// Note :first-child & :last-child do NOT have a parameter
Str pseudoSelector := Str<| (?::(first-child|lang|last-child|nth-child|nth-last-child)(?:\((\w+)\))?)? |>.trim
return Regex.fromStr("\\s+${typeSelector}${idSelector}${classesSelector}${attrSelector}${pseudoSelector}")
}
** Queries the document with the given CSS selector any returns any matching elements.
**
** Throws 'ParseErr' if the CSS selector is invalid and 'checked' is 'true'.
SElem[] select(Str cssSelector, Bool checked := true) {
// this is just a quick hack for now - but it gets me out of a hole
// if I need to support a complicated selector with a "," then I probably reconsider the selector!
cssSelector.split(',').flatMap { doSelect(it, checked) }
}
private SElem[] doSelect(Str cssSelector, Bool checked := true) {
// CASE-INSENSITIVITY
cssSelectorStr := " " + cssSelector.lower
if (checked)
selectorRegex.split(cssSelectorStr, 1000).each |leftovers| {
if (!leftovers.isEmpty)
throw ParseErr("CSS selector is not valid: ${cssSelector}")
}
matcher := selectorRegex.matcher(cssSelectorStr)
selectors := Selector[,]
while (matcher.find) {
selectors.add(Selector(matcher))
}
if (selectors.isEmpty)
return !checked ? SElem#.emptyList : throw ParseErr("CSS selector is not valid: ${cssSelector}")
possibles := rootBucket.select(selectors.last)
if (selectors.size == 1)
return possibles
survivors := possibles.findAll |SElem? elem -> Bool| {
selectors[0..<-1].reverse.all |sel -> Bool| {
elem = findMatch(elem, sel)
return elem != null
}
}
return survivors
}
** Queries the document for elements under the given parent, returning any matches.
**
** Throws 'ParseErr' if the CSS selector is invalid and 'checked' is 'true'.
SElem[] selectFrom(SElem parent, Str cssSelector, Bool checked := true) {
survivors := select(cssSelector, checked)
// make sure our base node is in the hierarchy
survivors = survivors.findAll |SElem? elem->Bool| {
survivor := false
while (survivor == false && elem != null) {
survivor = elem.parent == parent
elem = elem.parent
}
return survivor
}
return survivors
}
** An alias for 'select()'
@Operator
SElem[] get(Str cssSelector, Bool checked := true) {
select(cssSelector, checked)
}
** Adds another root element.
Void add(SElem elem) {
rootBucket.add(elem)
}
** Updates / refreshes the given SElem - must have already been added.
Void update(SElem elem, Bool recurse := false) {
rootBucket.update(elem, recurse)
}
** Removed the the given SElem.
Void remove(SElem elem) {
rootBucket.remove(elem, true)
}
private SElem? findMatch(SElem? elem, Selector selector) {
if (selector.combinator == Combinator.descendant) {
elem = elem?.parent
while (elem != null && matches(elem, selector) == null) {
elem = elem?.parent
}
return elem
}
if (selector.combinator == Combinator.child) {
elem = elem?.parent
return matches(elem, selector)
}
if (selector.combinator == Combinator.sibling) {
parent := elem?.parent
if (parent == null)
return null
index := parent.children.index(elem)
if (index < 1)
return null
elem = parent.children.getSafe(index - 1)
return matches(elem, selector)
}
return null
}
private SElem? matches(SElem? elem, Selector selector) {
if (elem == null)
return null
return NodeBucketSingle(elem).select(selector)
}
}