stew-select
Version:
CSS selectors that allow regular expressions. Stew is a meatier soup.
296 lines (275 loc) • 10.9 kB
text/coffeescript
#
# **PredicateFactory** generates boolean-valued
# functions that implement tests of specific
# CSS selectors.
#
# Each generated function has the signature:
#
# predicate(node,node_metadata,dom_metadata)
#
# and returns `true` iff the given `node` matches
# the associated CSS selection rule.
#
# (This is an internal class, primarily used by
# the class `Stew`. These methods are subject to
# change without notice.)
#
class PredicateFactory
# **and_predicate** generates a function that returns `true` iff *all* of the given `predicates` evaluate to `true`.
and_predicate:(predicates)->
return (node,node_metadata,dom_metadata)->
for predicate in predicates
if not predicate(node,node_metadata,dom_metadata)
return false
return true
# **or_predicate** generates a function that returns `true` iff *any* of the given `predicates` evaluate to `true`.
or_predicate:(predicates)->
return (node,node_metadata,dom_metadata)->
for predicate in predicates
if predicate(node,node_metadata,dom_metadata)
return true
return false
# **by_attribute_predicate** creates a predicate
# that returns `true` if the given `attrname`
# matches the given `attrvalue`.
#
# * When `attrvalue` is `null`, then the predicate will
# return `true` if the tested node has an attribute
# named `attrname`.
#
# * When `attrvalue` is a String then the predicate will
# return true if the value of the `attrname` attribute
# *equals* the `attrvalue` *string*.
#
# * When `attrvalue` is a `RegExp` then the predicate will
# return `true` if the value of the `attrname` attribute
# *matches* the `attrvalue` *expression*.
#
# * When `valuedelim` is non-`null`, the specified value will
# be used as a delimiter by which to split the value of
# the `attrname` attribute, and the corresponding elements
# will be tested rather than the entire string.
#
# For example, the call:
#
# by_attribute_predicate('class','foo',/\s+/)
#
# will return a function that tests if a given DOM node
# has been assigned class `foo`. E.g, `true` for these:
#
# <span class="foo"></span>
#
# <span class="bar foo"></span>
#
# and `false` for these:
#
# <span></span>
#
# <span class="food"></span>
#
by_attribute_predicate:(attrname,attrvalue=null,valuedelim=null)->
if typeof(attrname) is 'string'
np = (str)->str is attrname
else
np = (str)->attrname.test(str)
if attrvalue is null
vp = null
else if typeof(attrvalue) is 'string'
attrvalue = attrvalue.replace(/\\\"/g,'"')
vp = (str)->str is attrvalue
else if attrvalue?.test?
vp = (str)->attrvalue.test(str)
return (node)->
for name,value of node?.attribs
if np(name)
if vp is null
return true
else
if valuedelim?
if value?
for token in value.split(valuedelim)
if vp(token)
return true
else
if vp(value)
return true
return false
# **by_class_predicate** creates a predicate
# that returns `true` if the given DOM node has
# the specified `klass` value.
by_class_predicate:(klass)=>
return @by_attribute_predicate('class',klass,/\s+/)
# **by_id_predicate** creates a predicate
# that returns `true` if the given DOM node has
# the specified `id` value.
by_id_predicate:(id)=>
return @by_attribute_predicate('id',id)
# **by_attr_exists_predicate** creates a
# predicate that returns `true` if the given DOM
# node has an attribute with the specified `attrname`,
# regardless of the value for the atttribute.
by_attr_exists_predicate:(attrname)=>
return @by_attribute_predicate(attrname,null)
# **by_attr_value_predicate** is an alias to `by_attribute_predicate`.
by_attr_value_predicate:(attrname,attrvalue,valuedelim)=>
return @by_attribute_predicate(attrname,attrvalue,valuedelim)
# **_escape_for_regexp** is an internal utility function that escapes
# reserved characters to create a string that can be embedded
# in a regular expression.
_escape_for_regexp:(str)->return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1")
# **by_attr_value_pipe_equals** creates a predicate that
# implements the `[name|=value]` CSS selector (matching tags
# with a `name` attribute with a value matching `value`
# (exactly) or a value that starts with `value` followed
# by a `-` character..
#
# (Used for selectors such as `[lang|=en]`, for example,
# which will match the values `en`, `en-US` and `en-CA`.)
#
# When `attrvalue` is a regular expression:
#
# - If `attrvalue` doesn't already start with
# `^` (matching the beginning of a line),
# then `^` will be added.
#
# - If `attrvalue` doesn't already end with
# `($|-)` (matching the end of a line, or `-`)
# then `($|-)` will be added.
#
# Hence the regular expression `/f[aeio]o?/`
# would be converted to `/^f[aeio]o?($|-)/` but
# the regular expression `/^en($|-)/` would be
# left alone.
by_attr_value_pipe_equals:(attrname,attrvalue)=>
if typeof attrvalue is 'string'
regexp_source = @_escape_for_regexp(attrvalue)
attrvalue = new RegExp("^#{regexp_source}($|-)")
else
regexp_source = attrvalue.source
modifier = ''
modifier += 'i' if attrvalue.ignoreCase
modifier += 'g' if attrvalue.global
modifier += 'm' if attrvalue.multiline
unless /^\^/.test attrvalue.source
regexp_source = "^#{regexp_source}"
unless /\(\$\|-\)$/.test regexp_source
regexp_source = "#{regexp_source}($|-)"
attrvalue = new RegExp(regexp_source,modifier)
return @by_attribute_predicate(attrname,attrvalue)
# **by_tag_predicate** creates a
# predicate that returns `true` if the given DOM
# node is a tag with the specified `name`.
#
# If `name` is a RegExp then the predicate will
# return true if tag's name *matches* the
# specified *expression*.
#
# If `name` is a String then the predicate will
# return true if the tag's name *equals* the
# specified *string*.
by_tag_predicate:(name)->
if typeof name is 'string'
return (node)->(name is node.name)
else
return (node)->(name.test(node.name))
# **first_child_predicate** returns a predicate that evaluates to `true`
# iff the given `node` is the first child *tag* node among all of
# its siblings.
#
#{ TODO FIXME should :first-child also consider elements like <script>?
first_child_predicate:()->return @_first_child_impl
_first_child_impl:(node,node_metadata,dom_metadata)->
if node.type is 'tag' and node_metadata.siblings?
for elt in node_metadata.siblings
if elt.type is 'tag'
return node._stew_node_id is elt._stew_node_id
return false
# **any_tag_predicate** returns a predicate that evaluates
# to `true` iff the given `node` is a tag.
any_tag_predicate:()->return @_any_tag_impl
# (...and **_any_tag_impl** is the implementation of that predicate.)
_any_tag_impl:(node)->(node?.type is 'tag')
# **descendant_predicate**
# returns a predicate that for the given array
# *P* containing *n*, evaluates to `true` for
# a given `node` if:
#
# - `P[n-1](node)` is `true`, and
#
# - `P[n-2](parent)` is `true` for some element
# `parent` that is an ancestor of `node`
#
# - `P[n-3](parent2)` is `true` for some element
# `parent2` that is an ancestor of `parent`
#
# - ...etc.
#
# In other words, the returned predicate will evalue to `true`
# for the current `node` if each of the given `predicates`
# evaluates to `true` for some ancestor of the node, *in sequence*.
# (I.e., the node that matches `predicates[n]` must be an ancestor
# of the node that mathces `predicates[n+1]`.)
#
descendant_predicate:(predicates)->
if predicates.length is 1
return predicates[0]
else
return (node,node_metadata,dom_metadata)->
if predicates[predicates.length-1](node,node_metadata,dom_metadata)
cloned_path = [].concat(node_metadata.path)
cloned_predicates = [].concat(predicates)
cloned_predicates.pop() # drop last predicate, we just tested it
while cloned_path.length > 0
node = cloned_path.pop()
node_metadata = dom_metadata[node._stew_node_id]
if cloned_predicates[cloned_predicates.length-1](node,node_metadata,dom_metadata)
cloned_predicates.pop()
if cloned_predicates.length is 0
return true
break
return false
# **direct_descendant_predicate** returns a predicate
# that evaluates to `true` iff `child_selector` evalutes
# to `true` for the given `node` and `parent_selector` evalutes
# to `true` for the given `node`'s parent.
direct_descendant_predicate:(parent_selector,child_selector)->
return (node,node_metadata,dom_metadata)->
if child_selector(node,node_metadata,dom_metadata)
parent = node_metadata.parent
parent_metadata = dom_metadata[parent._stew_node_id]
return parent_selector(parent,parent_metadata,dom_metadata)
return false
# **adjacent_sibling_predicate** returns a predicate
# that evaluates to `true` iff `second` evaluates
# to `true` for the given `node` and `first` evaluates
# to `true` for the tag sibling immediately preceding
# the given `node`.
adjacent_sibling_predicate:(first,second)->
return (node,node_metadata,dom_metadata)->
if second(node,node_metadata,dom_metadata)
prev_tag_index = node_metadata.sib_index - 1
while prev_tag_index > 0
if node_metadata.siblings[prev_tag_index].type is 'tag'
prev_tag = node_metadata.siblings[prev_tag_index]
return first(prev_tag,dom_metadata[prev_tag._stew_node_id],dom_metadata)
else
prev_tag_index -= 1
return false
# **preceding_sibling_predicate** returns a predicate
# that evaluates to `true` iff `second` evaluates
# to `true` for the given `node` and `first` evaluates
# to `true` for some tag sibling preceding
# the given `node`.
preceding_sibling_predicate:(first,second)->
return (node,node_metadata,dom_metadata)->
if second(node,node_metadata,dom_metadata)
for prev,index in node_metadata.siblings
if index is node_metadata.sib_index
return false
else if prev.type is 'tag'
if first(prev,dom_metadata[prev._stew_node_id],dom_metadata)
return true
return false
# The PredicateFactory class is exported under the name `PredicateFactory`.
exports = exports ? this
exports.PredicateFactory = PredicateFactory