stew-select
Version:
CSS selectors that allow regular expressions. Stew is a meatier soup.
562 lines (430 loc) • 26.7 kB
HTML
<html>
<head>
<title>predicate-factory.coffee</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
<link rel="stylesheet" media="all" href="docco.css" />
</head>
<body>
<div id="container">
<div id="background"></div>
<ul id="jump_to">
<li>
<a class="large" href="javascript:void(0);">Jump To …</a>
<a class="small" href="javascript:void(0);">+</a>
<div id="jump_wrapper">
<div id="jump_page">
<a class="source" href="dom-util.html">
dom-util.coffee
</a>
<a class="source" href="predicate-factory.html">
predicate-factory.coffee
</a>
<a class="source" href="stew.html">
stew.coffee
</a>
</div>
</li>
</ul>
<ul class="sections">
<li id="title">
<div class="annotation">
<h1>predicate-factory.coffee</h1>
</div>
</li>
<li id="section-1">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-1">¶</a>
</div>
<p><strong>PredicateFactory</strong> generates boolean-valued
functions that implement tests of specific
CSS selectors.</p>
<p>Each generated function has the signature:</p>
<pre><code>predicate(node,node_metadata,dom_metadata)</code></pre>
<p>and returns <code>true</code> iff the given <code>node</code> matches
the associated CSS selection rule.</p>
<p>(This is an internal class, primarily used by
the class <code>Stew</code>. These methods are subject to
change without notice.)</p>
</div>
<div class="content"><div class='highlight'><pre><span class="class"><span class="keyword">class</span> <span class="title">PredicateFactory</span></span></pre></div></div>
</li>
<li id="section-2">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-2">¶</a>
</div>
<p><strong>and_predicate</strong> generates a function that returns <code>true</code> iff <em>all</em> of the given <code>predicates</code> evaluate to <code>true</code>.</p>
</div>
<div class="content"><div class='highlight'><pre> and_predicate:(predicates)->
<span class="keyword">return</span> (node,node_metadata,dom_metadata)->
<span class="keyword">for</span> predicate <span class="keyword">in</span> predicates
<span class="keyword">if</span> <span class="keyword">not</span> predicate(node,node_metadata,dom_metadata)
<span class="keyword">return</span> <span class="literal">false</span>
<span class="keyword">return</span> <span class="literal">true</span></pre></div></div>
</li>
<li id="section-3">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-3">¶</a>
</div>
<p><strong>or_predicate</strong> generates a function that returns <code>true</code> iff <em>any</em> of the given <code>predicates</code> evaluate to <code>true</code>.</p>
</div>
<div class="content"><div class='highlight'><pre> or_predicate:(predicates)->
<span class="keyword">return</span> (node,node_metadata,dom_metadata)->
<span class="keyword">for</span> predicate <span class="keyword">in</span> predicates
<span class="keyword">if</span> predicate(node,node_metadata,dom_metadata)
<span class="keyword">return</span> <span class="literal">true</span>
<span class="keyword">return</span> <span class="literal">false</span></pre></div></div>
</li>
<li id="section-4">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-4">¶</a>
</div>
<p><strong>by_attribute_predicate</strong> creates a predicate
that returns <code>true</code> if the given <code>attrname</code>
matches the given <code>attrvalue</code>.</p>
<ul>
<li><p>When <code>attrvalue</code> is <code>null</code>, then the predicate will
return <code>true</code> if the tested node has an attribute
named <code>attrname</code>.</p>
</li>
<li><p>When <code>attrvalue</code> is a String then the predicate will
return true if the value of the <code>attrname</code> attribute
<em>equals</em> the <code>attrvalue</code> <em>string</em>.</p>
</li>
<li><p>When <code>attrvalue</code> is a <code>RegExp</code> then the predicate will
return <code>true</code> if the value of the <code>attrname</code> attribute
<em>matches</em> the <code>attrvalue</code> <em>expression</em>.</p>
</li>
<li><p>When <code>valuedelim</code> is non-<code>null</code>, the specified value will
be used as a delimiter by which to split the value of
the <code>attrname</code> attribute, and the corresponding elements
will be tested rather than the entire string.</p>
</li>
</ul>
<p>For example, the call:</p>
<pre><code>by_attribute_predicate('class','foo',/\s+/)</code></pre>
<p>will return a function that tests if a given DOM node
has been assigned class <code>foo</code>. E.g, <code>true</code> for these:</p>
<pre><code><span class="foo"></span>
<span class="bar foo"></span></code></pre>
<p>and <code>false</code> for these:</p>
<pre><code><span></span>
<span class="food"></span></code></pre>
</div>
<div class="content"><div class='highlight'><pre> by_attribute_predicate:(attrname,attrvalue=<span class="literal">null</span>,valuedelim=<span class="literal">null</span>)->
<span class="keyword">if</span> <span class="keyword">typeof</span>(attrname) <span class="keyword">is</span> <span class="string">'string'</span>
<span class="function"><span class="title">np</span></span> = (str)->str <span class="keyword">is</span> attrname
<span class="keyword">else</span>
<span class="function"><span class="title">np</span></span> = (str)->attrname.test(str)
<span class="keyword">if</span> attrvalue <span class="keyword">is</span> <span class="literal">null</span>
vp = <span class="literal">null</span>
<span class="keyword">else</span> <span class="keyword">if</span> <span class="keyword">typeof</span>(attrvalue) <span class="keyword">is</span> <span class="string">'string'</span>
attrvalue = attrvalue.replace(<span class="regexp">/\\\"/g</span>,<span class="string">'"'</span>)
<span class="function"><span class="title">vp</span></span> = (str)->str <span class="keyword">is</span> attrvalue
<span class="keyword">else</span> <span class="keyword">if</span> attrvalue?.test?
<span class="function"><span class="title">vp</span></span> = (str)->attrvalue.test(str)
<span class="keyword">return</span> (node)->
<span class="keyword">for</span> name,value <span class="keyword">of</span> node?.attribs
<span class="keyword">if</span> np(name)
<span class="keyword">if</span> vp <span class="keyword">is</span> <span class="literal">null</span>
<span class="keyword">return</span> <span class="literal">true</span>
<span class="keyword">else</span>
<span class="keyword">if</span> valuedelim?
<span class="keyword">if</span> value?
<span class="keyword">for</span> token <span class="keyword">in</span> value.split(valuedelim)
<span class="keyword">if</span> vp(token)
<span class="keyword">return</span> <span class="literal">true</span>
<span class="keyword">else</span>
<span class="keyword">if</span> vp(value)
<span class="keyword">return</span> <span class="literal">true</span>
<span class="keyword">return</span> <span class="literal">false</span></pre></div></div>
</li>
<li id="section-5">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-5">¶</a>
</div>
<p><strong>by_class_predicate</strong> creates a predicate
that returns <code>true</code> if the given DOM node has
the specified <code>klass</code> value.</p>
</div>
<div class="content"><div class='highlight'><pre> by_class_predicate:(klass)=>
<span class="keyword">return</span> <span class="property">@by_attribute_predicate</span>(<span class="string">'class'</span>,klass,<span class="regexp">/\s+/</span>)</pre></div></div>
</li>
<li id="section-6">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-6">¶</a>
</div>
<p><strong>by_id_predicate</strong> creates a predicate
that returns <code>true</code> if the given DOM node has
the specified <code>id</code> value.</p>
</div>
<div class="content"><div class='highlight'><pre> by_id_predicate:(id)=>
<span class="keyword">return</span> <span class="property">@by_attribute_predicate</span>(<span class="string">'id'</span>,id)</pre></div></div>
</li>
<li id="section-7">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-7">¶</a>
</div>
<p><strong>by_attr_exists_predicate</strong> creates a
predicate that returns <code>true</code> if the given DOM
node has an attribute with the specified <code>attrname</code>,
regardless of the value for the atttribute.</p>
</div>
<div class="content"><div class='highlight'><pre> by_attr_exists_predicate:(attrname)=>
<span class="keyword">return</span> <span class="property">@by_attribute_predicate</span>(attrname,<span class="literal">null</span>)</pre></div></div>
</li>
<li id="section-8">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-8">¶</a>
</div>
<p><strong>by_attr_value_predicate</strong> is an alias to <code>by_attribute_predicate</code>.</p>
</div>
<div class="content"><div class='highlight'><pre> by_attr_value_predicate:(attrname,attrvalue,valuedelim)=>
<span class="keyword">return</span> <span class="property">@by_attribute_predicate</span>(attrname,attrvalue,valuedelim)</pre></div></div>
</li>
<li id="section-9">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-9">¶</a>
</div>
<p><strong>_escape_for_regexp</strong> is an internal utility function that escapes
reserved characters to create a string that can be embedded
in a regular expression.</p>
</div>
<div class="content"><div class='highlight'><pre> _escape_for_regexp:(str)-><span class="keyword">return</span> str.replace(<span class="regexp">/([.?*+^$[\]\\(){}|-])/g</span>, <span class="string">"\\$1"</span>)</pre></div></div>
</li>
<li id="section-10">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-10">¶</a>
</div>
<p><strong>by_attr_value_pipe_equals</strong> creates a predicate that
implements the <code>[name|=value]</code> CSS selector (matching tags
with a <code>name</code> attribute with a value matching <code>value</code>
(exactly) or a value that starts with <code>value</code> followed
by a <code>-</code> character..</p>
<p>(Used for selectors such as <code>[lang|=en]</code>, for example,
which will match the values <code>en</code>, <code>en-US</code> and <code>en-CA</code>.)</p>
<p>When <code>attrvalue</code> is a regular expression:</p>
<ul>
<li><p>If <code>attrvalue</code> doesn't already start with
<code>^</code> (matching the beginning of a line),
then <code>^</code> will be added.</p>
</li>
<li><p>If <code>attrvalue</code> doesn't already end with
<code>($|-)</code> (matching the end of a line, or <code>-</code>)
then <code>($|-)</code> will be added.</p>
</li>
</ul>
<p>Hence the regular expression <code>/f[aeio]o?/</code>
would be converted to <code>/^f[aeio]o?($|-)/</code> but
the regular expression <code>/^en($|-)/</code> would be
left alone.</p>
</div>
<div class="content"><div class='highlight'><pre> by_attr_value_pipe_equals:(attrname,attrvalue)=>
<span class="keyword">if</span> <span class="keyword">typeof</span> attrvalue <span class="keyword">is</span> <span class="string">'string'</span>
regexp_source = <span class="property">@_escape_for_regexp</span>(attrvalue)
attrvalue = <span class="keyword">new</span> RegExp(<span class="string">"^<span class="subst">#{regexp_source}</span>($|-)"</span>)
<span class="keyword">else</span>
regexp_source = attrvalue.source
modifier = <span class="string">''</span>
modifier += <span class="string">'i'</span> <span class="keyword">if</span> attrvalue.ignoreCase
modifier += <span class="string">'g'</span> <span class="keyword">if</span> attrvalue.global
modifier += <span class="string">'m'</span> <span class="keyword">if</span> attrvalue.multiline
<span class="keyword">unless</span> <span class="regexp">/^\^/</span>.test attrvalue.source
regexp_source = <span class="string">"^<span class="subst">#{regexp_source}</span>"</span>
<span class="keyword">unless</span> <span class="regexp">/\(\$\|-\)$/</span>.test regexp_source
regexp_source = <span class="string">"<span class="subst">#{regexp_source}</span>($|-)"</span>
attrvalue = <span class="keyword">new</span> RegExp(regexp_source,modifier)
<span class="keyword">return</span> <span class="property">@by_attribute_predicate</span>(attrname,attrvalue)</pre></div></div>
</li>
<li id="section-11">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-11">¶</a>
</div>
<p><strong>by_tag_predicate</strong> creates a
predicate that returns <code>true</code> if the given DOM
node is a tag with the specified <code>name</code>.</p>
<p>If <code>name</code> is a RegExp then the predicate will
return true if tag's name <em>matches</em> the
specified <em>expression</em>.</p>
<p>If <code>name</code> is a String then the predicate will
return true if the tag's name <em>equals</em> the
specified <em>string</em>.</p>
</div>
<div class="content"><div class='highlight'><pre> by_tag_predicate:(name)->
<span class="keyword">if</span> <span class="keyword">typeof</span> name <span class="keyword">is</span> <span class="string">'string'</span>
<span class="keyword">return</span> (node)->(name <span class="keyword">is</span> node.name)
<span class="keyword">else</span>
<span class="keyword">return</span> (node)->(name.test(node.name))</pre></div></div>
</li>
<li id="section-12">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-12">¶</a>
</div>
<p><strong>first_child_predicate</strong> returns a predicate that evaluates to <code>true</code>
iff the given <code>node</code> is the first child <em>tag</em> node among all of
its siblings.</p>
</div>
<div class="content"><div class='highlight'><pre> <span class="comment">#{ TODO FIXME should :first-child also consider elements like <script>?</span>
first_child_predicate:()-><span class="keyword">return</span> <span class="property">@_first_child_impl</span>
_first_child_impl:(node,node_metadata,dom_metadata)->
<span class="keyword">if</span> node.type <span class="keyword">is</span> <span class="string">'tag'</span> <span class="keyword">and</span> node_metadata.siblings?
<span class="keyword">for</span> elt <span class="keyword">in</span> node_metadata.siblings
<span class="keyword">if</span> elt.type <span class="keyword">is</span> <span class="string">'tag'</span>
<span class="keyword">return</span> node._stew_node_id <span class="keyword">is</span> elt._stew_node_id
<span class="keyword">return</span> <span class="literal">false</span></pre></div></div>
</li>
<li id="section-13">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-13">¶</a>
</div>
<p><strong>any_tag_predicate</strong> returns a predicate that evaluates
to <code>true</code> iff the given <code>node</code> is a tag.</p>
</div>
<div class="content"><div class='highlight'><pre> any_tag_predicate:()-><span class="keyword">return</span> <span class="property">@_any_tag_impl</span></pre></div></div>
</li>
<li id="section-14">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-14">¶</a>
</div>
<p>(...and <strong>_any_tag_impl</strong> is the implementation of that predicate.)</p>
</div>
<div class="content"><div class='highlight'><pre> _any_tag_impl:(node)->(node?.type <span class="keyword">is</span> <span class="string">'tag'</span>)</pre></div></div>
</li>
<li id="section-15">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-15">¶</a>
</div>
<p><strong>descendant_predicate</strong>
returns a predicate that for the given array
<em>P</em> containing <em>n</em>, evaluates to <code>true</code> for
a given <code>node</code> if:</p>
<ul>
<li><p><code>P[n-1](node)</code> is <code>true</code>, and</p>
</li>
<li><p><code>P[n-2](parent)</code> is <code>true</code> for some element
<code>parent</code> that is an ancestor of <code>node</code></p>
</li>
<li><p><code>P[n-3](parent2)</code> is <code>true</code> for some element
<code>parent2</code> that is an ancestor of <code>parent</code></p>
</li>
<li><p>...etc.</p>
</li>
</ul>
<p>In other words, the returned predicate will evalue to <code>true</code>
for the current <code>node</code> if each of the given <code>predicates</code>
evaluates to <code>true</code> for some ancestor of the node, <em>in sequence</em>.
(I.e., the node that matches <code>predicates[n]</code> must be an ancestor
of the node that mathces <code>predicates[n+1]</code>.)</p>
</div>
<div class="content"><div class='highlight'><pre> descendant_predicate:(predicates)->
<span class="keyword">if</span> predicates.length <span class="keyword">is</span> <span class="number">1</span>
<span class="keyword">return</span> predicates[<span class="number">0</span>]
<span class="keyword">else</span>
<span class="keyword">return</span> (node,node_metadata,dom_metadata)->
<span class="keyword">if</span> predicates[predicates.length-<span class="number">1</span>](node,node_metadata,dom_metadata)
cloned_path = [].concat(node_metadata.path)
cloned_predicates = [].concat(predicates)
cloned_predicates.pop() <span class="comment"># drop last predicate, we just tested it</span>
<span class="keyword">while</span> cloned_path.length > <span class="number">0</span>
node = cloned_path.pop()
node_metadata = dom_metadata[node._stew_node_id]
<span class="keyword">if</span> cloned_predicates[cloned_predicates.length-<span class="number">1</span>](node,node_metadata,dom_metadata)
cloned_predicates.pop()
<span class="keyword">if</span> cloned_predicates.length <span class="keyword">is</span> <span class="number">0</span>
<span class="keyword">return</span> <span class="literal">true</span>
<span class="keyword">break</span>
<span class="keyword">return</span> <span class="literal">false</span></pre></div></div>
</li>
<li id="section-16">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-16">¶</a>
</div>
<p><strong>direct_descendant_predicate</strong> returns a predicate
that evaluates to <code>true</code> iff <code>child_selector</code> evalutes
to <code>true</code> for the given <code>node</code> and <code>parent_selector</code> evalutes
to <code>true</code> for the given <code>node</code>'s parent.</p>
</div>
<div class="content"><div class='highlight'><pre> direct_descendant_predicate:(parent_selector,child_selector)->
<span class="keyword">return</span> (node,node_metadata,dom_metadata)->
<span class="keyword">if</span> child_selector(node,node_metadata,dom_metadata)
parent = node_metadata.parent
parent_metadata = dom_metadata[parent._stew_node_id]
<span class="keyword">return</span> parent_selector(parent,parent_metadata,dom_metadata)
<span class="keyword">return</span> <span class="literal">false</span></pre></div></div>
</li>
<li id="section-17">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-17">¶</a>
</div>
<p><strong>adjacent_sibling_predicate</strong> returns a predicate
that evaluates to <code>true</code> iff <code>second</code> evaluates
to <code>true</code> for the given <code>node</code> and <code>first</code> evaluates
to <code>true</code> for the tag sibling immediately preceding
the given <code>node</code>.</p>
</div>
<div class="content"><div class='highlight'><pre> adjacent_sibling_predicate:(first,second)->
<span class="keyword">return</span> (node,node_metadata,dom_metadata)->
<span class="keyword">if</span> second(node,node_metadata,dom_metadata)
prev_tag_index = node_metadata.sib_index - <span class="number">1</span>
<span class="keyword">while</span> prev_tag_index > <span class="number">0</span>
<span class="keyword">if</span> node_metadata.siblings[prev_tag_index].type <span class="keyword">is</span> <span class="string">'tag'</span>
prev_tag = node_metadata.siblings[prev_tag_index]
<span class="keyword">return</span> first(prev_tag,dom_metadata[prev_tag._stew_node_id],dom_metadata)
<span class="keyword">else</span>
prev_tag_index -= <span class="number">1</span>
<span class="keyword">return</span> <span class="literal">false</span></pre></div></div>
</li>
<li id="section-18">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-18">¶</a>
</div>
<p><strong>preceding_sibling_predicate</strong> returns a predicate
that evaluates to <code>true</code> iff <code>second</code> evaluates
to <code>true</code> for the given <code>node</code> and <code>first</code> evaluates
to <code>true</code> for some tag sibling preceding
the given <code>node</code>.</p>
</div>
<div class="content"><div class='highlight'><pre> preceding_sibling_predicate:(first,second)->
<span class="keyword">return</span> (node,node_metadata,dom_metadata)->
<span class="keyword">if</span> second(node,node_metadata,dom_metadata)
<span class="keyword">for</span> prev,index <span class="keyword">in</span> node_metadata.siblings
<span class="keyword">if</span> index <span class="keyword">is</span> node_metadata.sib_index
<span class="keyword">return</span> <span class="literal">false</span>
<span class="keyword">else</span> <span class="keyword">if</span> prev.type <span class="keyword">is</span> <span class="string">'tag'</span>
<span class="keyword">if</span> first(prev,dom_metadata[prev._stew_node_id],dom_metadata)
<span class="keyword">return</span> <span class="literal">true</span>
<span class="keyword">return</span> <span class="literal">false</span></pre></div></div>
</li>
<li id="section-19">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-19">¶</a>
</div>
<p>The PredicateFactory class is exported under the name <code>PredicateFactory</code>.</p>
</div>
<div class="content"><div class='highlight'><pre>exports = exports ? <span class="keyword">this</span>
exports.PredicateFactory = PredicateFactory</pre></div></div>
</li>
</ul>
</div>
</body>
</html>