UNPKG

stew-select

Version:

CSS selectors that allow regular expressions. Stew is a meatier soup.

296 lines (275 loc) 10.9 kB
# # **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