Skip to content

Instantly share code, notes, and snippets.

@jsocol
Forked from johan/README.md
Created January 28, 2013 05:34

Revisions

  1. @johan johan revised this gist Dec 17, 2012. 3 changed files with 0 additions and 272 deletions.
    1 change: 0 additions & 1 deletion tests.gitignore
    Original file line number Diff line number Diff line change
    @@ -1 +0,0 @@
    *.js
    239 changes: 0 additions & 239 deletions testson.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -1,239 +0,0 @@
    describe 'on()', ->
    fn = `on`

    it 'should throw an error on no input', ->
    try fn() catch e then err = e
    expect(err).toNotBe undefined

    it 'should expose an on.dom function after the first call', ->
    expect(typeof `on`.dom).toBe 'function'

    it 'should expose an on.query function after the first call', ->
    expect(typeof `on`.query).toBe 'function'

    it 'should expose an on.path_re function after the first call', ->
    expect(typeof `on`.path_re).toBe 'function'

    it 'should accept an object with "path_re", "dom", and/or "query" specs', ->
    what = fn( ready: (->), path_re: '/', dom: 'css *', query: true)
    expect(what).toEqual jasmine.any Array
    expect(what.length).toBeGreaterThan 2



    describe 'on.dom(dom spec – see below for the three types of dom spec)', ->
    it 'should be a function after the first on() call', ->
    try `on()` catch e then err = e
    expect(err).toNotBe undefined

    it 'should expose on.dom.* functions once on.dom() has run once', ->
    try
    `on`.dom()
    fns = Object.keys(`on`.dom).join(',')
    expect(fns).toBe 'css*,css+,css?,css,xpath*,xpath+,xpath?,xpath!,xpath'



    describe 'on.dom(dom spec type 1: a selector string)', ->
    root = document.documentElement
    assertion = it

    describe 'on.dom("css… selection"): Array/Node, optional/not?', ->
    describe 'Array of Node:s (0+ occurrences):', ->
    assertion 'on.dom("css* NotFound") => []', ->
    expect(`on`.dom('css* NotFound')).toEqual []

    assertion 'on.dom("css* html") => [root element]', ->
    expect(`on`.dom('css* html')).toEqual [root]

    assertion 'on.dom("css* *") => document.all (but as a proper Array)', ->
    what = `on`.dom("css* *")
    dall = [].slice.call document.all, 0
    expect(what).toEqual dall


    describe 'Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("css+ html") => [root element]', ->
    expect(`on`.dom('css+ html')).toEqual [root]

    assertion 'on.dom("css+ NotFound") => undefined', ->
    expect(`on`.dom('css+ NotFound')).toBe undefined


    describe 'single optional Node, or null if not found:', ->
    assertion 'on.dom("css? *") => root element (= first match)', ->
    expect(`on`.dom('css? *')).toBe root

    assertion 'on.dom("css? NotFound") => null (not found)', ->
    expect(`on`.dom('css? NotFound')).toBe null


    describe 'single mandatory Node:', ->
    assertion 'on.dom("css *") => the root element', ->
    expect(`on`.dom('css *')).toBe root

    assertion 'on.dom("css NotFound") => undefined (unsatisfied)', ->
    expect(`on`.dom('css NotFound')).toBe undefined



    describe 'on.dom("xpath… selection"): Array/Node, optional/not?', ->
    describe 'xpath* => Array of Node:s (0+ occurrences):', ->
    assertion 'on.dom("xpath* /*") => [root element]', ->
    expect(`on`.dom('xpath* /*')).toEqual [root]

    assertion 'on.dom("xpath* /NotFound") => []', ->
    expect(`on`.dom('xpath* /NotFound')).toEqual []


    describe 'xpath+ => Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("xpath+ /*") => [root element]', ->
    expect(`on`.dom('xpath+ /*')).toEqual [root]

    assertion 'on.dom("xpath+ /NotFound") => undefined', ->
    expect(`on`.dom('xpath+ /NotFound')).toBe undefined


    describe 'xpath? => single optional Node, or null if missing:', ->
    assertion 'on.dom("xpath? /NotFound") => null', ->
    expect(`on`.dom('xpath? /NotFound')).toBe null

    assertion 'on.dom("xpath? /*") => the root element', ->
    expect(`on`.dom('xpath? /*')).toBe root


    describe 'xpath => single mandatory Node:', ->
    assertion 'on.dom("xpath /*") => the root element', ->
    expect(`on`.dom('xpath /*')).toBe root

    assertion 'on.dom("xpath /NotFound") => undefined', ->
    expect(`on`.dom('xpath /NotFound')).toBe undefined

    assertion 'on.dom("xpath .") => the current document', ->
    expect(`on`.dom('xpath .')).toBe document


    describe '…or queries yielding Number/String/Boolean answers:', ->
    assertion 'on.dom("xpath count(/)") => 1', ->
    expect(`on`.dom('xpath count(/)')).toBe 1

    assertion 'on.dom("xpath count(/NotFound)") => 0', ->
    expect(`on`.dom('xpath count(/NotFound)')).toBe 0

    assertion 'on.dom("xpath name(/*)") => "html"', ->
    expect(`on`.dom('xpath name(/*)')).toBe 'html'

    assertion 'on.dom("xpath name(/)") => ""', ->
    expect(`on`.dom('xpath name(/)')).toBe ''

    assertion 'on.dom("xpath name(/*) = \'html\'") => true', ->
    expect(`on`.dom('xpath name(/*) = \'html\'')).toBe true

    assertion 'on.dom("xpath name(/*) = \'nope\'") => false', ->
    expect(`on`.dom('xpath name(/*) = \'nope\'')).toBe false


    describe 'xpath! makes assertions, requiring truthy answers:', ->
    assertion 'on.dom("xpath! count(/)") => 1', ->
    expect(`on`.dom('xpath count(/)')).toBe 1

    assertion 'on.dom("xpath! count(/NotFound)") => undefined', ->
    expect(`on`.dom('xpath! count(/NotFound)')).toBe undefined

    assertion 'on.dom("xpath! name(/*)") => "html"', ->
    expect(`on`.dom('xpath! name(/*)')).toBe 'html'

    assertion 'on.dom("xpath! name(/)") => undefined', ->
    expect(`on`.dom('xpath! name(/)')).toBe undefined

    assertion 'on.dom("xpath! name(/*) = \'html\'") => true', ->
    expect(`on`.dom('xpath! name(/*) = \'html\'')).toBe true

    assertion 'on.dom("xpath! name(/*) = \'nope\'") => undefined', ->
    expect(`on`.dom('xpath! name(/*) = \'nope\'')).toBe undefined



    describe 'on.dom(dom spec type 2: an object showing the structure you want)', ->
    html = document.documentElement
    head = document.querySelector 'head'
    try `on()` # ensures there's an on.dom to call
    assertion = it

    pluralize = (n, noun) -> "#{n} #{noun}#{if n is 1 then '' else 's'}"

    assertion 'on.dom({}) => {} (fairly useless, but minimal, test case)', ->
    expect(`on`.dom({})).toEqual {}

    assertion 'on.dom({ h:"css head", H:"css html" }) => { h:head, H:html }', ->
    expect(`on`.dom({ h:"css head", H:"css html" })).toEqual { h:head, H:html }

    assertion 'on.dom({ h:"css head", f:"css? foot" }) => { h:head, f:null }', ->
    expect(`on`.dom({ h:"css head", f:"css? foot" })).toEqual { h:head, f:null }

    assertion 'on.dom({ h:"css head", f:"css foot" }) => undefined (no foot!)', ->
    expect(`on`.dom({ h:"css head", f:"css foot" })).toEqual undefined

    assertion 'on.dom({ x:"css* frame" }) => { x:[] } (frames optional here)', ->
    expect(`on`.dom({ x:"css* frame" })).toEqual { x:[] }

    assertion 'on.dom({ x:"css+ frame" }) => undefined (but mandatory here!)', ->
    expect(`on`.dom({ x:"css+ frame" })).toBe undefined

    assertion 'on.dom({ x:"css* script" }) => { x:[…all (>=0) script tags…] }', ->
    what = `on`.dom({ x:"css* script" })
    expect(what.x).toEqual jasmine.any Array
    expect(what.x.every (s) -> s.nodeName is 'script')

    assertion 'on.dom({ x:"css+ script" }) => { x:[…all (>0) script tags…] }', ->
    what = `on`.dom({ x:"css+ script" })
    expect(what.x).toEqual jasmine.any Array
    expect(what.x.length).toBeGreaterThan 0
    expect(what.x.every (s) -> s.nodeName.toLowerCase() is 'script').toBe true

    assertion 'on.dom({ c:"xpath count(//script)" }) => {c:N} (any N is okay)', ->
    what = `on`.dom({ c:"xpath count(//script)" })
    expect(what).toEqual jasmine.any Object
    expect(N = what.c).toEqual jasmine.any Number
    console.log "on.dom({ c: count(…) }) found #{pluralize N, 'script'}"
    delete what.c
    expect(what).toEqual {}

    assertion 'on.dom({ c:"xpath! count(//script)" }) => {c:N} (only N!=0 ok)', ->
    what = `on`.dom({ c:"xpath! count(//script)" })
    expect(what.c).toBeGreaterThan 0
    delete what.c
    expect(what).toEqual {}

    assertion 'on.dom({ c:"xpath! count(//missing)" }) => undefined (as N==0)', ->
    expect(`on`.dom({ c:"xpath! count(//missing)" })).toBe undefined

    assertion 'on.dom({ c:"xpath! count(//*) and /html" }) => { c:true }', ->
    expect(`on`.dom({ c:"xpath! count(//*) > 5 and /html" })).toEqual c: true



    describe 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->
    html = document.documentElement
    head = document.querySelector 'head'
    try `on()` # ensures there's an on.dom to call
    assertion = it

    assertion 'on.dom(["css* script[src]", "xpath string(@src)"]) => ["url"…]', ->
    what = `on`.dom(["css* script[src]", "xpath string(@src)"])
    expect(what).toEqual jasmine.any Array
    expect(what.every (s) -> typeof s is 'string').toBe true

    assertion 'on.dom(["css? script:not([src])", "xpath string(.)"]) => "js…"', ->
    what = `on`.dom(["css? script:not([src])", "xpath string(.)"])
    expect(typeof what).toBe 'string'
    desc = 'Code of first inline script tag'
    console.log "#{desc}:\n#{what}\n(#{desc} ends.)"

    assertion 'on.dom(["css? script:not([src])", "xpath! string(@src)"])' +
    ' => undefined (empty string is not truthy => not a match)', ->
    what = `on`.dom(["css? script:not([src])", "xpath! string(@src)"])
    expect(what).toBe undefined

    assertion 'on.dom(["xpath /svg", "css* *"]) => undefined (not an svg doc)', ->
    expect(`on`.dom(["xpath /svg", "css* *"])).toBe undefined
    32 changes: 0 additions & 32 deletions testson.query.coffee
    Original file line number Diff line number Diff line change
    @@ -1,32 +0,0 @@
    describe 'on.query', ->
    q_was = location.search
    query = (q) ->
    if location.search isnt q
    url = location.href.replace /(\?[^#]*)?(#.*)?$/, "#{q}$2"
    history.replaceState history.state, document.title, url

    it 'should be a function after the first on() call', ->
    try `on()`
    expect(typeof `on`.query).toBe 'function'

    it 'on.query() => {} for a missing query string', ->
    query ''
    expect(`on`.query()).toEqual {}

    it 'on.query() => {} for an empty query string ("?")', ->
    query '?'
    expect(`on`.query()).toEqual {}

    it 'on.query() => { a:"", x:"0" } for a query string "?a=&x=0"', ->
    query '?a=&x=0'
    expect(`on`.query()).toEqual
    a: ''
    x: '0'

    it 'on.query() => { ugh:undefined } for a query string "?ugh"', ->
    query '?ugh'
    result = `on`.query()
    expect('ugh' of result).toBe true
    expect(result).toEqual {} # FIXME - better test framework?
    expect(result.ugh).toBe `undefined`
    query q_was # reset, for good measure
  2. @johan johan revised this gist Nov 29, 2012. 2 changed files with 25 additions and 21 deletions.
    42 changes: 22 additions & 20 deletions tests/on.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,6 @@
    describe 'on()', ->
    section = ((describe) -> (name, test) -> describe "#{name}\t", test)(describe)

    section 'on()', ->
    fn = `on`

    it 'should throw an error on no input', ->
    @@ -21,7 +23,7 @@ describe 'on()', ->



    describe 'on.dom(dom spec – see below for the three types of dom spec)', ->
    section 'on.dom(dom spec – see below for the three types of dom spec)', ->
    it 'should be a function after the first on() call', ->
    try `on()` catch e then err = e
    expect(err).toNotBe undefined
    @@ -34,16 +36,16 @@ describe 'on.dom(dom spec – see below for the three types of dom spec)', ->



    describe 'on.dom(dom spec type 1: a selector string)', ->
    section 'on.dom(dom spec type 1: a selector string)', ->
    root = document.documentElement
    assertion = it

    describe 'on.dom("css… selection"): Array/Node, optional/not?', ->
    describe 'Array of Node:s (0+ occurrences):', ->
    section 'on.dom("css… selection"): Array/Node, optional/not?', ->
    section 'Array of Node:s (0+ occurrences):', ->
    assertion 'on.dom("css* NotFound") => []', ->
    expect(`on`.dom('css* NotFound')).toEqual []

    assertion 'on.dom("css* html") => [root element]', ->
    assertion 'on.dom("css* html") => [html]', ->
    expect(`on`.dom('css* html')).toEqual [root]

    assertion 'on.dom("css* *") => document.all (but as a proper Array)', ->
    @@ -52,23 +54,23 @@ describe 'on.dom(dom spec type 1: a selector string)', ->
    expect(what).toEqual dall


    describe 'Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("css+ html") => [root element]', ->
    section 'Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("css+ html") => [html]', ->
    expect(`on`.dom('css+ html')).toEqual [root]

    assertion 'on.dom("css+ NotFound") => undefined', ->
    expect(`on`.dom('css+ NotFound')).toBe undefined


    describe 'single optional Node, or null if not found:', ->
    section 'single optional Node, or null if not found:', ->
    assertion 'on.dom("css? *") => root element (= first match)', ->
    expect(`on`.dom('css? *')).toBe root

    assertion 'on.dom("css? NotFound") => null (not found)', ->
    expect(`on`.dom('css? NotFound')).toBe null


    describe 'single mandatory Node:', ->
    section 'single mandatory Node:', ->
    assertion 'on.dom("css *") => the root element', ->
    expect(`on`.dom('css *')).toBe root

    @@ -77,32 +79,32 @@ describe 'on.dom(dom spec type 1: a selector string)', ->



    describe 'on.dom("xpath… selection"): Array/Node, optional/not?', ->
    describe 'xpath* => Array of Node:s (0+ occurrences):', ->
    section 'on.dom("xpath… selection"): Array/Node, optional/not?', ->
    section 'xpath* => Array of Node:s (0+ occurrences):', ->
    assertion 'on.dom("xpath* /*") => [root element]', ->
    expect(`on`.dom('xpath* /*')).toEqual [root]

    assertion 'on.dom("xpath* /NotFound") => []', ->
    expect(`on`.dom('xpath* /NotFound')).toEqual []


    describe 'xpath+ => Array of Node:s (1+ occurrences):', ->
    section 'xpath+ => Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("xpath+ /*") => [root element]', ->
    expect(`on`.dom('xpath+ /*')).toEqual [root]

    assertion 'on.dom("xpath+ /NotFound") => undefined', ->
    expect(`on`.dom('xpath+ /NotFound')).toBe undefined


    describe 'xpath? => single optional Node, or null if missing:', ->
    section 'xpath? => single optional Node, or null if missing:', ->
    assertion 'on.dom("xpath? /NotFound") => null', ->
    expect(`on`.dom('xpath? /NotFound')).toBe null

    assertion 'on.dom("xpath? /*") => the root element', ->
    expect(`on`.dom('xpath? /*')).toBe root


    describe 'xpath => single mandatory Node:', ->
    section 'xpath => single mandatory Node:', ->
    assertion 'on.dom("xpath /*") => the root element', ->
    expect(`on`.dom('xpath /*')).toBe root

    @@ -113,7 +115,7 @@ describe 'on.dom(dom spec type 1: a selector string)', ->
    expect(`on`.dom('xpath .')).toBe document


    describe '…or queries yielding Number/String/Boolean answers:', ->
    section '…or queries yielding Number/String/Boolean answers:', ->
    assertion 'on.dom("xpath count(/)") => 1', ->
    expect(`on`.dom('xpath count(/)')).toBe 1

    @@ -133,7 +135,7 @@ describe 'on.dom(dom spec type 1: a selector string)', ->
    expect(`on`.dom('xpath name(/*) = \'nope\'')).toBe false


    describe 'xpath! makes assertions, requiring truthy answers:', ->
    section 'xpath! makes assertions, requiring truthy answers:', ->
    assertion 'on.dom("xpath! count(/)") => 1', ->
    expect(`on`.dom('xpath count(/)')).toBe 1

    @@ -154,7 +156,7 @@ describe 'on.dom(dom spec type 1: a selector string)', ->



    describe 'on.dom(dom spec type 2: an object showing the structure you want)', ->
    section 'on.dom(dom spec type 2: an object showing the structure you want)', ->
    html = document.documentElement
    head = document.querySelector 'head'
    try `on()` # ensures there's an on.dom to call
    @@ -213,7 +215,7 @@ describe 'on.dom(dom spec type 2: an object showing the structure you want)', ->



    describe 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->
    section 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->
    html = document.documentElement
    head = document.querySelector 'head'
    try `on()` # ensures there's an on.dom to call
    @@ -245,7 +247,7 @@ describe 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->
    expect(`on`.dom([[head, html], "xpath ."])).toEqual [head, html]


    describe 'on.dom plugins:', ->
    section 'on.dom plugins:', ->
    html = document.documentElement
    assertion = it
    fn = `on`
    4 changes: 3 additions & 1 deletion tests/on.query.coffee
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,6 @@
    describe 'on.query', ->
    section = ((describe) -> (name, test) -> describe "#{name}\t", test)(describe)

    section 'on.query', ->
    q_was = location.search
    query = (q) ->
    if location.search isnt q
  3. @johan johan revised this gist Nov 25, 2012. 2 changed files with 46 additions and 5 deletions.
    18 changes: 13 additions & 5 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -103,7 +103,7 @@
    */

    function on(opts) {
    function on(opts, plugins) {
    var Object_toString = Object.prototype.toString
    , Array_slice = Array.prototype.slice
    , FAIL = 'dom' in on ? undefined : (function() {
    @@ -145,8 +145,7 @@ function on(opts) {
    , load = get('load')
    , pushState = get('pushstate')
    , pjax_event = get('pjaxevent')
    , parse_dom_rule // regexp on.dom uses to recognize its various sub-commands
    , name, rule, test, result, retry
    , name, rule, test, result, retry, plugin
    ;

    if (typeof ready !== 'function' &&
    @@ -156,6 +155,15 @@ function on(opts) {
    throw new Error('on() needs at least a "ready" or "load" function!');
    }

    if (plugins)
    for (name in plugins)
    if ((rule = plugins[name]) && (test = on[name]))
    for (plugin in rule)
    if (!(test[plugin])) {
    on._parse_dom_rule = null;
    test[plugin] = rule[plugin];
    }

    if (pushState && history.pushState &&
    (on.pushState = on.pushState || []).indexOf(opts) === -1) {
    on.pushState.push(opts); // make sure we don't re-register after navigation
    @@ -318,9 +326,9 @@ function on(opts) {
    // fall-through
    default: throw new Error('non-String dom match rule: '+ rule);
    }
    if (!parse_dom_rule) parse_dom_rule = new RegExp('^(' +
    if (!on._parse_dom_rule) on._parse_dom_rule = new RegExp('^(' +
    Object.keys(on.dom).map(quoteRe).join('|') + ')\\s*(.*)');
    var match = parse_dom_rule.exec(rule), type, func;
    var match = on._parse_dom_rule.exec(rule), type, func;
    if (match) {
    type = match[1];
    rule = match[2];
    33 changes: 33 additions & 0 deletions tests/on.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -243,3 +243,36 @@ describe 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->

    assertion 'on.dom([[head, html], "xpath ."]) => [head, html]', ->
    expect(`on`.dom([[head, html], "xpath ."])).toEqual [head, html]


    describe 'on.dom plugins:', ->
    html = document.documentElement
    assertion = it
    fn = `on`

    assertion 'on( { dom: "my_plugin", ready: ready = (x) -> }\n' +
    ' , { dom: "my_plugin": -> document.body }\n' +
    ' ) => ready(document.body)', ->
    fn( { dom: "my_plugin", ready: ready = jasmine.createSpy 'ready' }
    , { dom: "my_plugin": -> document.body }
    )
    expect(ready).toHaveBeenCalledWith(document.body)

    assertion 'on.dom(["my_plugin", "xpath ."]) => body', ->
    expect(`on`.dom(["my_plugin", "xpath ."])).toBe document.body

    assertion 'on.dom(["my_plugin", "xpath .."]) => html', ->
    expect(`on`.dom(["my_plugin", "xpath .."])).toBe html

    assertion 'on.dom("xpath .") => document', ->
    expect(`on`.dom("xpath .")).toBe document

    ###
    assertion 'on( { dom: ["my_plugin", "xpath ."], ready: ready = (x) -> }\n' +
    ' , { dom: my_plugin: -> document.body })\n' +
    '=> ready(body)', ->
    ready = jasmine.createSpy 'ready'
    fn( { dom: ["my_plugin", "xpath ."], ready: ready }
    , { dom: my_plugin: -> document.body })
    expect(ready).toHaveBeenCalledWith(document.body)
    ###
  4. @johan johan revised this gist Nov 25, 2012. 2 changed files with 12 additions and 2 deletions.
    8 changes: 6 additions & 2 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -312,8 +312,12 @@ function on(opts) {
    // returns null if it didn't find optional matches at this level
    // returns Node or an Array of nodes, or a basic type from some XPath query
    function lookup(rule) {
    if (typeof rule !== 'string')
    throw new Error('non-String dom match rule: '+ rule);
    switch (typeof rule) {
    case 'string': break; // main case - rest of function
    case 'object': if ('nodeType' in rule || rule.length) return rule;
    // fall-through
    default: throw new Error('non-String dom match rule: '+ rule);
    }
    if (!parse_dom_rule) parse_dom_rule = new RegExp('^(' +
    Object.keys(on.dom).map(quoteRe).join('|') + ')\\s*(.*)');
    var match = parse_dom_rule.exec(rule), type, func;
    6 changes: 6 additions & 0 deletions tests/on.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -237,3 +237,9 @@ describe 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->

    assertion 'on.dom(["xpath /svg", "css* *"]) => undefined (not an svg doc)', ->
    expect(`on`.dom(["xpath /svg", "css* *"])).toBe undefined

    assertion 'on.dom([html, "xpath ."]) => html', ->
    expect(`on`.dom([html, "xpath ."])).toBe html

    assertion 'on.dom([[head, html], "xpath ."]) => [head, html]', ->
    expect(`on`.dom([[head, html], "xpath ."])).toEqual [head, html]
  5. @johan johan revised this gist Nov 25, 2012. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -288,9 +288,9 @@ function on(opts) {
    var doc = this.evaluate ? this : this.ownerDocument, next;
    var got = doc.evaluate(xpath, this, null, 0, null), all = [];
    switch (got.resultType) {
    case got.STRING_TYPE: return got.stringValue;
    case got.NUMBER_TYPE: return got.numberValue;
    case got.BOOLEAN_TYPE: return got.booleanValue;
    case 1/*XPathResult.NUMBER_TYPE*/: return got.numberValue;
    case 2/*XPathResult.STRING_TYPE*/: return got.stringValue;
    case 3/*XPathResult.BOOLEAN_TYPE*/: return got.booleanValue;
    default: while ((next = got.iterateNext())) all.push(next); return all;
    }
    }
  6. @johan johan revised this gist Nov 25, 2012. 1 changed file with 8 additions and 8 deletions.
    16 changes: 8 additions & 8 deletions tests/on.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -48,7 +48,7 @@ describe 'on.dom(dom spec type 1: a selector string)', ->

    assertion 'on.dom("css* *") => document.all (but as a proper Array)', ->
    what = `on`.dom("css* *")
    dall = [].slice.call document.all, 0
    dall = [].slice.call document.getElementsByTagName('*'), 0
    expect(what).toEqual dall


    @@ -120,14 +120,14 @@ describe 'on.dom(dom spec type 1: a selector string)', ->
    assertion 'on.dom("xpath count(/NotFound)") => 0', ->
    expect(`on`.dom('xpath count(/NotFound)')).toBe 0

    assertion 'on.dom("xpath name(/*)") => "html"', ->
    expect(`on`.dom('xpath name(/*)')).toBe 'html'
    assertion 'on.dom("xpath name(/*)") => "html" or "HTML"', ->
    expect(`on`.dom('xpath name(/*)')).toMatch /^(html|HTML)$/

    assertion 'on.dom("xpath name(/)") => ""', ->
    expect(`on`.dom('xpath name(/)')).toBe ''

    assertion 'on.dom("xpath name(/*) = \'html\'") => true', ->
    expect(`on`.dom('xpath name(/*) = \'html\'')).toBe true
    assertion 'on.dom("xpath count(/*) = 1") => true', ->
    expect(`on`.dom('xpath count(/*) = 1')).toBe true

    assertion 'on.dom("xpath name(/*) = \'nope\'") => false', ->
    expect(`on`.dom('xpath name(/*) = \'nope\'')).toBe false
    @@ -141,13 +141,13 @@ describe 'on.dom(dom spec type 1: a selector string)', ->
    expect(`on`.dom('xpath! count(/NotFound)')).toBe undefined

    assertion 'on.dom("xpath! name(/*)") => "html"', ->
    expect(`on`.dom('xpath! name(/*)')).toBe 'html'
    expect(`on`.dom('xpath! name(/*)')).toMatch /^(html|HTML)$/

    assertion 'on.dom("xpath! name(/)") => undefined', ->
    expect(`on`.dom('xpath! name(/)')).toBe undefined

    assertion 'on.dom("xpath! name(/*) = \'html\'") => true', ->
    expect(`on`.dom('xpath! name(/*) = \'html\'')).toBe true
    assertion 'on.dom("xpath! count(/*) = 1") => true', ->
    expect(`on`.dom('xpath! count(/*) = 1')).toBe true

    assertion 'on.dom("xpath! name(/*) = \'nope\'") => undefined', ->
    expect(`on`.dom('xpath! name(/*) = \'nope\'')).toBe undefined
  7. @johan johan revised this gist Nov 25, 2012. 1 changed file with 9 additions and 5 deletions.
    14 changes: 9 additions & 5 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -60,7 +60,7 @@ about what this buys you
    The DOM API:s are not only verbose,
    but also cripple results with bad types!
    From `on.js` array selectors, you get a
    real `Array`s that you can operate on with
    real `Array` that you can operate on with
    `.forEach`, `.map`, `.some`, `.every`,
    `.filter`, `.reduce`, `.slice`, `.indexOf`,
    and all the other really awesome
    @@ -71,7 +71,8 @@ about what this buys you
    As a script's complexity grows,
    so does the burden of maintaining it.
    With `on.js`, this is still true
    – but it grows far slower!<br>
    – but it grows far slower!

    Seeing (and naming) the structure
    of the input your code operates on
    helps reading and writing the code
    @@ -81,7 +82,8 @@ about what this buys you

    Maybe you later want to whip up
    another script using the same data,
    or even make a shell web scraper for it?<br>
    or even make a shell web scraper for it?

    Copy, paste, done!
    You're in business – in seconds.

    @@ -90,7 +92,8 @@ about what this buys you
    Web sites change.
    This makes user scripts break,
    start doing the wrong thing, or
    filling your console with errors.<br>
    filling your console with errors.

    When they change
    so your script
    no longer has the stuff it needs,
    @@ -99,7 +102,8 @@ about what this buys you

    If you don't miss it, great!
    – that site has improved, and
    your script will never bother it.<br>
    your script will never bother it.

    If you do – just update
    its `on.js` preamble,
    and your code just
  8. @johan johan revised this gist Nov 25, 2012. 1 changed file with 14 additions and 2 deletions.
    16 changes: 14 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -48,12 +48,24 @@ about what this buys you
    Optimize for happiness!
    This is of course personal,
    but I love how clean `on.js`
    makes my user scripts.<br>
    makes my user scripts.

    I can see what they do,
    at a moment's glance, and
    what data they depend on.
    Maintenance gets easier.

    * **Sane native javascript types**

    The DOM API:s are not only verbose,
    but also cripple results with bad types!
    From `on.js` array selectors, you get a
    real `Array`s that you can operate on with
    `.forEach`, `.map`, `.some`, `.every`,
    `.filter`, `.reduce`, `.slice`, `.indexOf`,
    and all the other really awesome
    `Array.prototype` methods.

    * **More complex scripts become feasible**

    As a script's complexity grows,
    @@ -121,4 +133,4 @@ test'em will pick it up, too.

    They are currently written in
    [jasmine](http://pivotal.github.com/jasmine/),
    lightly tweaked for improved readability.
    lightly tweaked for improved readability.
  9. @johan johan revised this gist Nov 24, 2012. 1 changed file with 26 additions and 28 deletions.
    54 changes: 26 additions & 28 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,17 +1,17 @@
    # on.js
    # [on.js](http://git.io/on.js)

    The fun part of user scripting
    is deciding what happens.
    The boring part
    is deciding what _happens_.
    The _boring_ part
    is scavenging the DOM
    for bits of templated data
    or nodes you want to mod.
    for bits of templated data,
    or elements you want to mod.

    Have `on.js` do it for you!
    [MIT licensed](http://en.wikipedia.org/wiki/MIT_License), or
    [Copyheart](http://copyheart.org/):
    Copying is an act of love.
    Please copy and share.
    copying is an act of love.
    Please copy and share!

    ## Quick Example

    @@ -37,18 +37,18 @@ function nukeCouponless(offers) {

    ## What's the benefit?

    **Separate logic from page structure**
    **Separate logic from page structure.**

    I could talk all night
    about what this buys you
    – here are a few:
    – here are some highlights:

    * **Beautiful code**

    I optimize for happiness.
    Optimize for happiness!
    This is of course personal,
    but I love how clean `on.js`
    makes my user scripts.
    makes my user scripts.<br>
    I can see what they do,
    at a moment's glance, and
    what data they depend on.
    @@ -59,17 +59,17 @@ about what this buys you
    As a script's complexity grows,
    so does the burden of maintaining it.
    With `on.js`, this is still true
    – but it grows much more slowly!
    – but it grows far slower!<br>
    Seeing (and naming) the structure
    of the input your code operates on
    helps reading and writing the code
    that operates on it.
    operating on it.

    * **Reuse one script's page structure awareness**

    Maybe you later want to whip up
    another script using the same data,
    or even make a shell web scraper for it?
    or even make a shell web scraper for it?<br>
    Copy, paste, done!
    You're in business – in seconds.

    @@ -78,26 +78,24 @@ about what this buys you
    Web sites change.
    This makes user scripts break,
    start doing the wrong thing, or
    filling your console with errors.

    filling your console with errors.<br>
    When they change
    to the point where your script
    so your script
    no longer has the stuff it needs,
    your code will just never run,
    your code will just never run
    instead of breaking the page.

    If you don't miss it, great!
    – that site has improved, and
    your script will never bother it.

    If you do, just update
    its `on.js` preamble
    – and your code just
    your script will never bother it.<br>
    If you do – just update
    its `on.js` preamble,
    and your code just
    magically works again.

    ## Tests

    `on.js` has a test suite!
    `on.js` comes with a test suite.
    It uses [node.js](http://nodejs.org),
    [test'em](https://github.com/airportyh/testem) and
    [coffee-script](coffeescript.org).
    @@ -106,13 +104,13 @@ run this to install the latter two:

    npm install testem coffee-script -g

    And then you can run it like this, for instance:
    Then you can run it like this, for instance:

    testem -l chrome

    This, incidentally, gives pretty good docs
    Incidentally, it provides pretty good docs
    about all the ways you can slice a page,
    and and how they all work.
    and and how they work.

    New tests are easy to add.
    Just stick them in the appropriate
    @@ -123,4 +121,4 @@ test'em will pick it up, too.

    They are currently written in
    [jasmine](http://pivotal.github.com/jasmine/),
    lightly tweaked for improved readability.
    lightly tweaked for improved readability.
  10. @johan johan revised this gist Nov 24, 2012. 4 changed files with 398 additions and 0 deletions.
    126 changes: 126 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,126 @@
    # on.js

    The fun part of user scripting
    is deciding what happens.
    The boring part
    is scavenging the DOM
    for bits of templated data
    or nodes you want to mod.

    Have `on.js` do it for you!
    [MIT licensed](http://en.wikipedia.org/wiki/MIT_License), or
    [Copyheart](http://copyheart.org/):
    Copying is an act of love.
    Please copy and share.

    ## Quick Example

    Want to sift away noise at [retailmenot](http://www.retailmenot.com/view/vayama.com)?

    ```js
    // hide the noise at http://www.retailmenot.com/view/vayama.com
    on({ dom: [ 'css* #offers li.offer'
    , { deal: 'xpath .'
    , coupon: 'css? .crux .code'
    }
    ]
    , ready: nukeCouponless
    });

    function nukeCouponless(offers) {
    offers.forEach(function sift(a) {
    if (!a.coupon)
    a.deal.style.display = 'none';
    });
    }
    ```

    ## What's the benefit?

    **Separate logic from page structure**

    I could talk all night
    about what this buys you
    – here are a few:

    * **Beautiful code**

    I optimize for happiness.
    This is of course personal,
    but I love how clean `on.js`
    makes my user scripts.
    I can see what they do,
    at a moment's glance, and
    what data they depend on.
    Maintenance gets easier.

    * **More complex scripts become feasible**

    As a script's complexity grows,
    so does the burden of maintaining it.
    With `on.js`, this is still true
    – but it grows much more slowly!
    Seeing (and naming) the structure
    of the input your code operates on
    helps reading and writing the code
    that operates on it.

    * **Reuse one script's page structure awareness**

    Maybe you later want to whip up
    another script using the same data,
    or even make a shell web scraper for it?
    Copy, paste, done!
    You're in business – in seconds.

    * **Have your scripts magically self-deprecate**

    Web sites change.
    This makes user scripts break,
    start doing the wrong thing, or
    filling your console with errors.

    When they change
    to the point where your script
    no longer has the stuff it needs,
    your code will just never run,
    instead of breaking the page.

    If you don't miss it, great!
    – that site has improved, and
    your script will never bother it.

    If you do, just update
    its `on.js` preamble
    – and your code just
    magically works again.

    ## Tests

    `on.js` has a test suite!
    It uses [node.js](http://nodejs.org),
    [test'em](https://github.com/airportyh/testem) and
    [coffee-script](coffeescript.org).
    Once node is installed,
    run this to install the latter two:

    npm install testem coffee-script -g

    And then you can run it like this, for instance:

    testem -l chrome

    This, incidentally, gives pretty good docs
    about all the ways you can slice a page,
    and and how they all work.

    New tests are easy to add.
    Just stick them in the appropriate
    `.coffee` file in `tests/`,
    or make a new `.js` or `.coffee` file
    in the same directory, and
    test'em will pick it up, too.

    They are currently written in
    [jasmine](http://pivotal.github.com/jasmine/),
    lightly tweaked for improved readability.
    1 change: 1 addition & 0 deletions tests.gitignore
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    *.js
    239 changes: 239 additions & 0 deletions testson.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,239 @@
    describe 'on()', ->
    fn = `on`

    it 'should throw an error on no input', ->
    try fn() catch e then err = e
    expect(err).toNotBe undefined

    it 'should expose an on.dom function after the first call', ->
    expect(typeof `on`.dom).toBe 'function'

    it 'should expose an on.query function after the first call', ->
    expect(typeof `on`.query).toBe 'function'

    it 'should expose an on.path_re function after the first call', ->
    expect(typeof `on`.path_re).toBe 'function'

    it 'should accept an object with "path_re", "dom", and/or "query" specs', ->
    what = fn( ready: (->), path_re: '/', dom: 'css *', query: true)
    expect(what).toEqual jasmine.any Array
    expect(what.length).toBeGreaterThan 2



    describe 'on.dom(dom spec – see below for the three types of dom spec)', ->
    it 'should be a function after the first on() call', ->
    try `on()` catch e then err = e
    expect(err).toNotBe undefined

    it 'should expose on.dom.* functions once on.dom() has run once', ->
    try
    `on`.dom()
    fns = Object.keys(`on`.dom).join(',')
    expect(fns).toBe 'css*,css+,css?,css,xpath*,xpath+,xpath?,xpath!,xpath'



    describe 'on.dom(dom spec type 1: a selector string)', ->
    root = document.documentElement
    assertion = it

    describe 'on.dom("css… selection"): Array/Node, optional/not?', ->
    describe 'Array of Node:s (0+ occurrences):', ->
    assertion 'on.dom("css* NotFound") => []', ->
    expect(`on`.dom('css* NotFound')).toEqual []

    assertion 'on.dom("css* html") => [root element]', ->
    expect(`on`.dom('css* html')).toEqual [root]

    assertion 'on.dom("css* *") => document.all (but as a proper Array)', ->
    what = `on`.dom("css* *")
    dall = [].slice.call document.all, 0
    expect(what).toEqual dall


    describe 'Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("css+ html") => [root element]', ->
    expect(`on`.dom('css+ html')).toEqual [root]

    assertion 'on.dom("css+ NotFound") => undefined', ->
    expect(`on`.dom('css+ NotFound')).toBe undefined


    describe 'single optional Node, or null if not found:', ->
    assertion 'on.dom("css? *") => root element (= first match)', ->
    expect(`on`.dom('css? *')).toBe root

    assertion 'on.dom("css? NotFound") => null (not found)', ->
    expect(`on`.dom('css? NotFound')).toBe null


    describe 'single mandatory Node:', ->
    assertion 'on.dom("css *") => the root element', ->
    expect(`on`.dom('css *')).toBe root

    assertion 'on.dom("css NotFound") => undefined (unsatisfied)', ->
    expect(`on`.dom('css NotFound')).toBe undefined



    describe 'on.dom("xpath… selection"): Array/Node, optional/not?', ->
    describe 'xpath* => Array of Node:s (0+ occurrences):', ->
    assertion 'on.dom("xpath* /*") => [root element]', ->
    expect(`on`.dom('xpath* /*')).toEqual [root]

    assertion 'on.dom("xpath* /NotFound") => []', ->
    expect(`on`.dom('xpath* /NotFound')).toEqual []


    describe 'xpath+ => Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("xpath+ /*") => [root element]', ->
    expect(`on`.dom('xpath+ /*')).toEqual [root]

    assertion 'on.dom("xpath+ /NotFound") => undefined', ->
    expect(`on`.dom('xpath+ /NotFound')).toBe undefined


    describe 'xpath? => single optional Node, or null if missing:', ->
    assertion 'on.dom("xpath? /NotFound") => null', ->
    expect(`on`.dom('xpath? /NotFound')).toBe null

    assertion 'on.dom("xpath? /*") => the root element', ->
    expect(`on`.dom('xpath? /*')).toBe root


    describe 'xpath => single mandatory Node:', ->
    assertion 'on.dom("xpath /*") => the root element', ->
    expect(`on`.dom('xpath /*')).toBe root

    assertion 'on.dom("xpath /NotFound") => undefined', ->
    expect(`on`.dom('xpath /NotFound')).toBe undefined

    assertion 'on.dom("xpath .") => the current document', ->
    expect(`on`.dom('xpath .')).toBe document


    describe '…or queries yielding Number/String/Boolean answers:', ->
    assertion 'on.dom("xpath count(/)") => 1', ->
    expect(`on`.dom('xpath count(/)')).toBe 1

    assertion 'on.dom("xpath count(/NotFound)") => 0', ->
    expect(`on`.dom('xpath count(/NotFound)')).toBe 0

    assertion 'on.dom("xpath name(/*)") => "html"', ->
    expect(`on`.dom('xpath name(/*)')).toBe 'html'

    assertion 'on.dom("xpath name(/)") => ""', ->
    expect(`on`.dom('xpath name(/)')).toBe ''

    assertion 'on.dom("xpath name(/*) = \'html\'") => true', ->
    expect(`on`.dom('xpath name(/*) = \'html\'')).toBe true

    assertion 'on.dom("xpath name(/*) = \'nope\'") => false', ->
    expect(`on`.dom('xpath name(/*) = \'nope\'')).toBe false


    describe 'xpath! makes assertions, requiring truthy answers:', ->
    assertion 'on.dom("xpath! count(/)") => 1', ->
    expect(`on`.dom('xpath count(/)')).toBe 1

    assertion 'on.dom("xpath! count(/NotFound)") => undefined', ->
    expect(`on`.dom('xpath! count(/NotFound)')).toBe undefined

    assertion 'on.dom("xpath! name(/*)") => "html"', ->
    expect(`on`.dom('xpath! name(/*)')).toBe 'html'

    assertion 'on.dom("xpath! name(/)") => undefined', ->
    expect(`on`.dom('xpath! name(/)')).toBe undefined

    assertion 'on.dom("xpath! name(/*) = \'html\'") => true', ->
    expect(`on`.dom('xpath! name(/*) = \'html\'')).toBe true

    assertion 'on.dom("xpath! name(/*) = \'nope\'") => undefined', ->
    expect(`on`.dom('xpath! name(/*) = \'nope\'')).toBe undefined



    describe 'on.dom(dom spec type 2: an object showing the structure you want)', ->
    html = document.documentElement
    head = document.querySelector 'head'
    try `on()` # ensures there's an on.dom to call
    assertion = it

    pluralize = (n, noun) -> "#{n} #{noun}#{if n is 1 then '' else 's'}"

    assertion 'on.dom({}) => {} (fairly useless, but minimal, test case)', ->
    expect(`on`.dom({})).toEqual {}

    assertion 'on.dom({ h:"css head", H:"css html" }) => { h:head, H:html }', ->
    expect(`on`.dom({ h:"css head", H:"css html" })).toEqual { h:head, H:html }

    assertion 'on.dom({ h:"css head", f:"css? foot" }) => { h:head, f:null }', ->
    expect(`on`.dom({ h:"css head", f:"css? foot" })).toEqual { h:head, f:null }

    assertion 'on.dom({ h:"css head", f:"css foot" }) => undefined (no foot!)', ->
    expect(`on`.dom({ h:"css head", f:"css foot" })).toEqual undefined

    assertion 'on.dom({ x:"css* frame" }) => { x:[] } (frames optional here)', ->
    expect(`on`.dom({ x:"css* frame" })).toEqual { x:[] }

    assertion 'on.dom({ x:"css+ frame" }) => undefined (but mandatory here!)', ->
    expect(`on`.dom({ x:"css+ frame" })).toBe undefined

    assertion 'on.dom({ x:"css* script" }) => { x:[…all (>=0) script tags…] }', ->
    what = `on`.dom({ x:"css* script" })
    expect(what.x).toEqual jasmine.any Array
    expect(what.x.every (s) -> s.nodeName is 'script')

    assertion 'on.dom({ x:"css+ script" }) => { x:[…all (>0) script tags…] }', ->
    what = `on`.dom({ x:"css+ script" })
    expect(what.x).toEqual jasmine.any Array
    expect(what.x.length).toBeGreaterThan 0
    expect(what.x.every (s) -> s.nodeName.toLowerCase() is 'script').toBe true

    assertion 'on.dom({ c:"xpath count(//script)" }) => {c:N} (any N is okay)', ->
    what = `on`.dom({ c:"xpath count(//script)" })
    expect(what).toEqual jasmine.any Object
    expect(N = what.c).toEqual jasmine.any Number
    console.log "on.dom({ c: count(…) }) found #{pluralize N, 'script'}"
    delete what.c
    expect(what).toEqual {}

    assertion 'on.dom({ c:"xpath! count(//script)" }) => {c:N} (only N!=0 ok)', ->
    what = `on`.dom({ c:"xpath! count(//script)" })
    expect(what.c).toBeGreaterThan 0
    delete what.c
    expect(what).toEqual {}

    assertion 'on.dom({ c:"xpath! count(//missing)" }) => undefined (as N==0)', ->
    expect(`on`.dom({ c:"xpath! count(//missing)" })).toBe undefined

    assertion 'on.dom({ c:"xpath! count(//*) and /html" }) => { c:true }', ->
    expect(`on`.dom({ c:"xpath! count(//*) > 5 and /html" })).toEqual c: true



    describe 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->
    html = document.documentElement
    head = document.querySelector 'head'
    try `on()` # ensures there's an on.dom to call
    assertion = it

    assertion 'on.dom(["css* script[src]", "xpath string(@src)"]) => ["url"…]', ->
    what = `on`.dom(["css* script[src]", "xpath string(@src)"])
    expect(what).toEqual jasmine.any Array
    expect(what.every (s) -> typeof s is 'string').toBe true

    assertion 'on.dom(["css? script:not([src])", "xpath string(.)"]) => "js…"', ->
    what = `on`.dom(["css? script:not([src])", "xpath string(.)"])
    expect(typeof what).toBe 'string'
    desc = 'Code of first inline script tag'
    console.log "#{desc}:\n#{what}\n(#{desc} ends.)"

    assertion 'on.dom(["css? script:not([src])", "xpath! string(@src)"])' +
    ' => undefined (empty string is not truthy => not a match)', ->
    what = `on`.dom(["css? script:not([src])", "xpath! string(@src)"])
    expect(what).toBe undefined

    assertion 'on.dom(["xpath /svg", "css* *"]) => undefined (not an svg doc)', ->
    expect(`on`.dom(["xpath /svg", "css* *"])).toBe undefined
    32 changes: 32 additions & 0 deletions testson.query.coffee
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,32 @@
    describe 'on.query', ->
    q_was = location.search
    query = (q) ->
    if location.search isnt q
    url = location.href.replace /(\?[^#]*)?(#.*)?$/, "#{q}$2"
    history.replaceState history.state, document.title, url

    it 'should be a function after the first on() call', ->
    try `on()`
    expect(typeof `on`.query).toBe 'function'

    it 'on.query() => {} for a missing query string', ->
    query ''
    expect(`on`.query()).toEqual {}

    it 'on.query() => {} for an empty query string ("?")', ->
    query '?'
    expect(`on`.query()).toEqual {}

    it 'on.query() => { a:"", x:"0" } for a query string "?a=&x=0"', ->
    query '?a=&x=0'
    expect(`on`.query()).toEqual
    a: ''
    x: '0'

    it 'on.query() => { ugh:undefined } for a query string "?ugh"', ->
    query '?ugh'
    result = `on`.query()
    expect('ugh' of result).toBe true
    expect(result).toEqual {} # FIXME - better test framework?
    expect(result.ugh).toBe `undefined`
    query q_was # reset, for good measure
  11. @johan johan revised this gist Nov 24, 2012. 1 changed file with 1 addition and 9 deletions.
    10 changes: 1 addition & 9 deletions on.min.js
    Original file line number Diff line number Diff line change
    @@ -1,9 +1 @@
    function on(d){function k(a){h[a]=void 0;return d[a]}function n(a){return"[object Array]"===A.call(a)}function B(a,b){var c=document.createElement("script"),f=document.documentElement,b=JSON.stringify(b||[]).slice(1,-1);c.textContent="("+a+")("+b+");";f.appendChild(c);f.removeChild(c)}function J(a){var b={};((this===on||this===window?location.search:this)||"").replace(/\+/g,"%20").split("&").forEach(function(a){if(a=/^\??([^=&]*)(?:=(.*))?/.exec(a)){var f,e;e=a[1];a=a[2];try{f=decodeURIComponent(e)}catch(g){f=
    unescape(e)}if(null!=(e=a))try{e=decodeURIComponent(a)}catch(d){e=unescape(a)}b[f]=e}});if(!0===a||null==a)return b;throw Error("bad query type "+typeof a+": "+a);}function K(a){n(a)||(a=n(a)?a:[a]);var b=a.shift();"string"===typeof b&&(b=RegExp(b));if(!(b instanceof RegExp))throw Error(typeof b+" was not a regexp: "+b);b=b.exec(this===on||this===window?location.pathname:this);if(null===b)return g;if(!a.length)return b;var c={};for(b.shift();a.length;)c[a.shift()]=b.shift();return c}function C(a){return function(b){var c=
    a.apply(this,arguments);return null!==c?c:g}}function D(a){return function(b){var c=a.apply(this,arguments);return c.length?c:g}}function E(a){a=this.querySelectorAll(a);return L.call(a,0)}function F(a){return this.querySelector(a)}function p(a){var b=(this.evaluate?this:this.ownerDocument).evaluate(a,this,null,0,null),c=[];switch(b.resultType){case b.STRING_TYPE:return b.stringValue;case b.NUMBER_TYPE:return b.numberValue;case b.BOOLEAN_TYPE:return b.booleanValue;default:for(;a=b.iterateNext();)c.push(a);
    return c}}function G(a){a=p.call(this,a);return a instanceof Array?a[0]||null:a}function M(a){return(a+"").replace(/([-$(-+.?[-^{|}])/g,"\\$1")}function m(a,b){function c(a){if("string"!==typeof a)throw Error("non-String dom match rule: "+a);u||(u=RegExp("^("+Object.keys(on.dom).map(M).join("|")+")\\s*(.*)"));var b=u.exec(a),c,d;b&&(c=b[1],a=b[2],d=m[c]);if(!d)throw Error("unknown dom match rule "+c+": "+a);return d.call(this,a)}var f,e,d;void 0===b&&(b=this===on||this===window?document:this);if(null===
    b||b===g)return g;if(n(b)){f=[];for(d=0;d<b.length;d++)e=m.call(b[d],a),e!==g&&f.push(e);return f}if("object"!==typeof b||!("nodeType"in b))throw Error("illegal context: "+b);if("string"===typeof a)return c.call(b,a);if(n(a))return b=c.call(b,a[0]),null===b||b===g?b:m.call(b,a[1]);if("[object Object]"===A.call(a)){f={};for(d in a){e=m.call(b,a[d]);if(e===g)return g;f[d]=e}return f}throw Error("dom spec was neither a String, Object nor Array: "+a);}var A=Object.prototype.toString,L=Array.prototype.slice;
    if(!("dom"in on)){var q=m,r=D(E),i=C(F),N=D(p),O=p,q={path_re:{fn:K},query:{fn:J},dom:{fn:q,my:{"css*":E,"css+":r,"css?":F,css:i,"xpath*":p,"xpath+":N,"xpath?":G,"xpath!":function(a){return O.apply(this,arguments)||g},xpath:C(G)}},inject:{fn:B}},j,l;for(j in q){i=q[j];r=i.fn;if(i=i.my)for(l in i)r[l]=i[l];on[j]=r}}var g=void 0,s=[],h=Object.create(d),H=k("debug");k("name");j=k("ready");var v=k("load"),w=k("pushstate");l=k("pjaxevent");var u,t,x,y,z,I;if("function"!==typeof j&&"function"!==typeof v&&
    "function"!==typeof w)throw alert("no on function"),Error('on() needs at least a "ready" or "load" function!');if(w&&history.pushState&&-1===(on.pushState=on.pushState||[]).indexOf(d))on.pushState.push(d),history.pushState.armed||(B(function(a){function b(){var a=document.createEvent("Events");a.initEvent("history.pushState",!1,!1);document.dispatchEvent(a)}var c=history.pushState;history.pushState=function(){if(a&&window.$&&$.pjax)$(document).one(a,b);else setTimeout(b,0);return c.apply(this,arguments)}},
    [l]),history.pushState.armed=l),I=function(){h=Object.create(d);h.load=h.pushstate=void 0;h.ready=w;on(h)},document.addEventListener("history.pushState",function(){H&&console.log("on.pushstate",location.pathname);I()},!1);try{for(t in h)if(x=h[t],void 0!==x){y=on[t];if(!y)throw Error('did not grok rule "'+t+'"!');z=y(x);if(z===g)return!1;s.push(z)}}catch(P){return H&&console.warn("on(debug): we didn't run because "+P.message),!1}j&&j.apply(d,s.concat());v&&window.addEventListener("load",function(){v.apply(d,
    s.concat())});return s.concat(d)}
    function on(d){function k(a){h[a]=void 0;return d[a]}function n(a){return"[object Array]"===A.call(a)}function B(a,b){var c=document.createElement("script"),f=document.documentElement,b=JSON.stringify(b||[]).slice(1,-1);c.textContent="("+a+")("+b+");";f.appendChild(c);f.removeChild(c)}function J(a){var b={};((this===on||this===window?location.search:this)||"").replace(/\+/g,"%20").split("&").forEach(function(a){if(a=/^\??([^=&]*)(?:=(.*))?/.exec(a)){var f,e;e=a[1];a=a[2];try{f=decodeURIComponent(e)}catch(g){f=unescape(e)}if(null!=(e=a))try{e=decodeURIComponent(a)}catch(d){e=unescape(a)}b[f]=e}});if(!0===a||null==a)return b;throw Error("bad query type "+typeof a+": "+a);}function K(a){n(a)||(a=n(a)?a:[a]);var b=a.shift();"string"===typeof b&&(b=RegExp(b));if(!(b instanceof RegExp))throw Error(typeof b+" was not a regexp: "+b);b=b.exec(this===on||this===window?location.pathname:this);if(null===b)return g;if(!a.length)return b;var c={};for(b.shift();a.length;)c[a.shift()]=b.shift();return c}function C(a){return function(b){var c=a.apply(this,arguments);return null!==c?c:g}}function D(a){return function(b){var c=a.apply(this,arguments);return c.length?c:g}}function E(a){a=this.querySelectorAll(a);return L.call(a,0)}function F(a){return this.querySelector(a)}function p(a){var b=(this.evaluate?this:this.ownerDocument).evaluate(a,this,null,0,null),c=[];switch(b.resultType){case b.STRING_TYPE:return b.stringValue;case b.NUMBER_TYPE:return b.numberValue;case b.BOOLEAN_TYPE:return b.booleanValue;default:for(;a=b.iterateNext();)c.push(a);return c}}function G(a){a=p.call(this,a);return a instanceof Array?a[0]||null:a}function M(a){return(a+"").replace(/([-$(-+.?[-^{|}])/g,"\\$1")}function m(a,b){function c(a){if("string"!==typeof a)throw Error("non-String dom match rule: "+a);u||(u=RegExp("^("+Object.keys(on.dom).map(M).join("|")+")\\s*(.*)"));var b=u.exec(a),c,d;b&&(c=b[1],a=b[2],d=m[c]);if(!d)throw Error("unknown dom match rule "+c+": "+a);return d.call(this,a)}var f,e,d;void 0===b&&(b=this===on||this===window?document:this);if(null===b||b===g)return g;if(n(b)){f=[];for(d=0;d<b.length;d++)e=m.call(b[d],a),e!==g&&f.push(e);return f}if("object"!==typeof b||!("nodeType"in b))throw Error("illegal context: "+b);if("string"===typeof a)return c.call(b,a);if(n(a))return b=c.call(b,a[0]),null===b||b===g?b:m.call(b,a[1]);if("[object Object]"===A.call(a)){f={};for(d in a){e=m.call(b,a[d]);if(e===g)return g;f[d]=e}return f}throw Error("dom spec was neither a String, Object nor Array: "+a);}var A=Object.prototype.toString,L=Array.prototype.slice;if(!("dom"in on)){var q=m,r=D(E),i=C(F),N=D(p),O=p,q={path_re:{fn:K},query:{fn:J},dom:{fn:q,my:{"css*":E,"css+":r,"css?":F,css:i,"xpath*":p,"xpath+":N,"xpath?":G,"xpath!":function(a){return O.apply(this,arguments)||g},xpath:C(G)}},inject:{fn:B}},j,l;for(j in q){i=q[j];r=i.fn;if(i=i.my)for(l in i)r[l]=i[l];on[j]=r}}var g=void 0,s=[],h=Object.create(d),H=k("debug");k("name");j=k("ready");var v=k("load"),w=k("pushstate");l=k("pjaxevent");var u,t,x,y,z,I;if("function"!==typeof j&&"function"!==typeof v&&"function"!==typeof w)throw alert("no on function"),Error('on() needs at least a "ready" or "load" function!');if(w&&history.pushState&&-1===(on.pushState=on.pushState||[]).indexOf(d))on.pushState.push(d),history.pushState.armed||(B(function(a){function b(){var a=document.createEvent("Events");a.initEvent("history.pushState",!1,!1);document.dispatchEvent(a)}var c=history.pushState;history.pushState=function(){if(a&&window.$&&$.pjax)$(document).one(a,b);else setTimeout(b,0);return c.apply(this,arguments)}},[l]),history.pushState.armed=l),I=function(){h=Object.create(d);h.load=h.pushstate=void 0;h.ready=w;on(h)},document.addEventListener("history.pushState",function(){H&&console.log("on.pushstate",location.pathname);I()},!1);try{for(t in h)if(x=h[t],void 0!==x){y=on[t];if(!y)throw Error('did not grok rule "'+t+'"!');z=y(x);if(z===g)return!1;s.push(z)}}catch(P){return H&&console.warn("on(debug): we didn't run because "+P.message),!1}j&&j.apply(d,s.concat());v&&window.addEventListener("load",function(){v.apply(d,s.concat())});return s.concat(d)}
  12. @johan johan revised this gist Nov 24, 2012. 1 changed file with 9 additions and 1 deletion.
    10 changes: 9 additions & 1 deletion on.min.js
    Original file line number Diff line number Diff line change
    @@ -1 +1,9 @@
    function on(d){function j(a){h[a]=void 0;return d[a]}function n(a){return"[object Array]"===A.call(a)}function B(a,b){var c=document.createElement("script"),f=document.documentElement,b=JSON.stringify(b||[]).slice(1,-1);c.textContent="("+a+")("+b+");";f.appendChild(c);f.removeChild(c)}function J(a){var b={};(this||"").replace(/\+/g,"%20").split("&").forEach(function(a){if(a=/^\??([^=]*)=(.*)/.exec(a)){var f,e,g=a[1],a=a[2];try{f=decodeURIComponent(g)}catch(d){f=unescape(g)}try{e=decodeURIComponent(a)}catch(h){e=unescape(a)}b[f]=e}});if(!0===a||null==a)return b;throw Error("bad query type "+typeof a+": "+a);}function K(a){n(a)||(a=n(a)?a:[a]);var b=a.shift();"string"===typeof b&&(b=RegExp(b));if(!(b instanceof RegExp))throw Error(typeof b+" was not a regexp: "+b);b=b.exec(this);if(null===b)return e;if(!a.length)return b;var c={};for(b.shift();a.length;)c[a.shift()]=b.shift();return c}function C(a){return function(b){var c=a.apply(this,arguments);return null!==c?c:e}}function D(a){return function(b){var c=a.apply(this,arguments);return c.length?c:e}}function E(a){a=this.querySelectorAll(a);return L.call(a,0)}function F(a){return this.querySelector(a)}function p(a){var b=(this.evaluate?this:this.ownerDocument).evaluate(a,this,null,0,null),c=[];switch(b.resultType){case b.STRING_TYPE:return b.stringValue;case b.NUMBER_TYPE:return b.numberValue;case b.BOOLEAN_TYPE:return b.booleanValue;default:for(;a=b.iterateNext();)c.push(a);return c}}function G(a){a=p.call(this,a);return a instanceof Array?a[0]||null:a}function l(a,b){function c(a){if("string"!==typeof a)throw Error("non-String dom match rule: "+a);var b=/^((?:css|xpath)[?+*!]?)\s+(.*)/.exec(a),c,d;b&&(c=b[1],a=b[2],d=l[c]);if(!d)throw Error("unknown dom match rule "+c+": "+a);return d.call(this,a)}var f,d,g;void 0===b&&(b=this);if(null===b||b===e)return e;if(n(b)){f=[];for(g=0;g<b.length;g++){d=l.call(b[g],a);if(d===e)return e;f.push(d)}return f}if("object"!==typeof b||!("nodeType"in b))throw Error("illegal context: "+b);if("string"===typeof a)return c.call(b,a);if(n(a))return b=c.call(b,a[0]),l.call(b,a[1]);if("[object Object]"===A.call(a)){f={};for(g in a){d=l.call(b,a[g]);if(d===e)return e;f[g]=d}return f}throw Error("dom spec was neither a String, Object nor Array: "+a);}var A=Object.prototype.toString,L=Array.prototype.slice;if(!("dom"in on)){var q={fn:K,self:location.pathname},m={fn:J,self:location.search},r=l,s=C(F),M=D(E),N=C(G),O=D(p),P=p,q={path_re:q,query:m,dom:{fn:r,self:document,my:{css:s,"css?":F,"css+":M,"css*":E,xpath:N,"xpath?":G,"xpath+":O,"xpath*":p,"xpath!":function(a){return P.apply(this,arguments)||e}}},inject:{fn:B}},i,k;for(i in q){m=q[i];r=m.fn;if(s=m.my)for(k in s)r[k]=s[k];on[i]=r.bind(m.self)}}var e=void 0,t=[],h=Object.create(d),H=j("debug");j("name");i=j("ready");var v=j("load"),w=j("pushstate");k=j("pjaxevent");var u,x,y,z,I;if("function"!==typeof i&&"function"!==typeof v&&"function"!==typeof w)throw alert("no on function"),Error('on() needs at least a "ready" or "load" function!');if(w&&history.pushState&&-1===(on.pushState=on.pushState||[]).indexOf(d))on.pushState.push(d),history.pushState.armed||(B(function(a){function b(){var a=document.createEvent("Events");a.initEvent("history.pushState",!1,!1);document.dispatchEvent(a)}var c=history.pushState;history.pushState=function(){if(a&&window.$&&$.pjax)$(document).one(a,b);else setTimeout(b,0);return c.apply(this,arguments)}},[k]),history.pushState.armed=k),I=function(){h=Object.create(d);h.load=h.pushstate=void 0;h.ready=w;on(h)},document.addEventListener("history.pushState",function(){H&&console.log("on.pushstate",location.pathname);I()},!1);try{for(u in h)if(x=h[u],void 0!==x){y=on[u];if(!y)throw Error('did not grok rule "'+u+'"!');z=y(x);if(z===e)return!1;t.push(z)}}catch(Q){return H&&console.warn("on(debug): we didn't run because "+Q.message),!1}i&&i.apply(d,t.concat());v&&window.addEventListener("load",function(){v.apply(d,t.concat())});return t.concat(d)};
    function on(d){function k(a){h[a]=void 0;return d[a]}function n(a){return"[object Array]"===A.call(a)}function B(a,b){var c=document.createElement("script"),f=document.documentElement,b=JSON.stringify(b||[]).slice(1,-1);c.textContent="("+a+")("+b+");";f.appendChild(c);f.removeChild(c)}function J(a){var b={};((this===on||this===window?location.search:this)||"").replace(/\+/g,"%20").split("&").forEach(function(a){if(a=/^\??([^=&]*)(?:=(.*))?/.exec(a)){var f,e;e=a[1];a=a[2];try{f=decodeURIComponent(e)}catch(g){f=
    unescape(e)}if(null!=(e=a))try{e=decodeURIComponent(a)}catch(d){e=unescape(a)}b[f]=e}});if(!0===a||null==a)return b;throw Error("bad query type "+typeof a+": "+a);}function K(a){n(a)||(a=n(a)?a:[a]);var b=a.shift();"string"===typeof b&&(b=RegExp(b));if(!(b instanceof RegExp))throw Error(typeof b+" was not a regexp: "+b);b=b.exec(this===on||this===window?location.pathname:this);if(null===b)return g;if(!a.length)return b;var c={};for(b.shift();a.length;)c[a.shift()]=b.shift();return c}function C(a){return function(b){var c=
    a.apply(this,arguments);return null!==c?c:g}}function D(a){return function(b){var c=a.apply(this,arguments);return c.length?c:g}}function E(a){a=this.querySelectorAll(a);return L.call(a,0)}function F(a){return this.querySelector(a)}function p(a){var b=(this.evaluate?this:this.ownerDocument).evaluate(a,this,null,0,null),c=[];switch(b.resultType){case b.STRING_TYPE:return b.stringValue;case b.NUMBER_TYPE:return b.numberValue;case b.BOOLEAN_TYPE:return b.booleanValue;default:for(;a=b.iterateNext();)c.push(a);
    return c}}function G(a){a=p.call(this,a);return a instanceof Array?a[0]||null:a}function M(a){return(a+"").replace(/([-$(-+.?[-^{|}])/g,"\\$1")}function m(a,b){function c(a){if("string"!==typeof a)throw Error("non-String dom match rule: "+a);u||(u=RegExp("^("+Object.keys(on.dom).map(M).join("|")+")\\s*(.*)"));var b=u.exec(a),c,d;b&&(c=b[1],a=b[2],d=m[c]);if(!d)throw Error("unknown dom match rule "+c+": "+a);return d.call(this,a)}var f,e,d;void 0===b&&(b=this===on||this===window?document:this);if(null===
    b||b===g)return g;if(n(b)){f=[];for(d=0;d<b.length;d++)e=m.call(b[d],a),e!==g&&f.push(e);return f}if("object"!==typeof b||!("nodeType"in b))throw Error("illegal context: "+b);if("string"===typeof a)return c.call(b,a);if(n(a))return b=c.call(b,a[0]),null===b||b===g?b:m.call(b,a[1]);if("[object Object]"===A.call(a)){f={};for(d in a){e=m.call(b,a[d]);if(e===g)return g;f[d]=e}return f}throw Error("dom spec was neither a String, Object nor Array: "+a);}var A=Object.prototype.toString,L=Array.prototype.slice;
    if(!("dom"in on)){var q=m,r=D(E),i=C(F),N=D(p),O=p,q={path_re:{fn:K},query:{fn:J},dom:{fn:q,my:{"css*":E,"css+":r,"css?":F,css:i,"xpath*":p,"xpath+":N,"xpath?":G,"xpath!":function(a){return O.apply(this,arguments)||g},xpath:C(G)}},inject:{fn:B}},j,l;for(j in q){i=q[j];r=i.fn;if(i=i.my)for(l in i)r[l]=i[l];on[j]=r}}var g=void 0,s=[],h=Object.create(d),H=k("debug");k("name");j=k("ready");var v=k("load"),w=k("pushstate");l=k("pjaxevent");var u,t,x,y,z,I;if("function"!==typeof j&&"function"!==typeof v&&
    "function"!==typeof w)throw alert("no on function"),Error('on() needs at least a "ready" or "load" function!');if(w&&history.pushState&&-1===(on.pushState=on.pushState||[]).indexOf(d))on.pushState.push(d),history.pushState.armed||(B(function(a){function b(){var a=document.createEvent("Events");a.initEvent("history.pushState",!1,!1);document.dispatchEvent(a)}var c=history.pushState;history.pushState=function(){if(a&&window.$&&$.pjax)$(document).one(a,b);else setTimeout(b,0);return c.apply(this,arguments)}},
    [l]),history.pushState.armed=l),I=function(){h=Object.create(d);h.load=h.pushstate=void 0;h.ready=w;on(h)},document.addEventListener("history.pushState",function(){H&&console.log("on.pushstate",location.pathname);I()},!1);try{for(t in h)if(x=h[t],void 0!==x){y=on[t];if(!y)throw Error('did not grok rule "'+t+'"!');z=y(x);if(z===g)return!1;s.push(z)}}catch(P){return H&&console.warn("on(debug): we didn't run because "+P.message),!1}j&&j.apply(d,s.concat());v&&window.addEventListener("load",function(){v.apply(d,
    s.concat())});return s.concat(d)}
  13. @johan johan revised this gist Nov 24, 2012. 1 changed file with 33 additions and 1 deletion.
    34 changes: 33 additions & 1 deletion tests/on.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -46,6 +46,11 @@ describe 'on.dom(dom spec type 1: a selector string)', ->
    assertion 'on.dom("css* html") => [root element]', ->
    expect(`on`.dom('css* html')).toEqual [root]

    assertion 'on.dom("css* *") => document.all (but as a proper Array)', ->
    what = `on`.dom("css* *")
    dall = [].slice.call document.all, 0
    expect(what).toEqual dall


    describe 'Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("css+ html") => [root element]', ->
    @@ -184,7 +189,7 @@ describe 'on.dom(dom spec type 2: an object showing the structure you want)', ->
    what = `on`.dom({ x:"css+ script" })
    expect(what.x).toEqual jasmine.any Array
    expect(what.x.length).toBeGreaterThan 0
    expect(what.x.every (s) -> s.nodeName is 'script')
    expect(what.x.every (s) -> s.nodeName.toLowerCase() is 'script').toBe true

    assertion 'on.dom({ c:"xpath count(//script)" }) => {c:N} (any N is okay)', ->
    what = `on`.dom({ c:"xpath count(//script)" })
    @@ -205,3 +210,30 @@ describe 'on.dom(dom spec type 2: an object showing the structure you want)', ->

    assertion 'on.dom({ c:"xpath! count(//*) and /html" }) => { c:true }', ->
    expect(`on`.dom({ c:"xpath! count(//*) > 5 and /html" })).toEqual c: true



    describe 'on.dom(dom spec type 3: [context_spec, per_match_spec])', ->
    html = document.documentElement
    head = document.querySelector 'head'
    try `on()` # ensures there's an on.dom to call
    assertion = it

    assertion 'on.dom(["css* script[src]", "xpath string(@src)"]) => ["url"…]', ->
    what = `on`.dom(["css* script[src]", "xpath string(@src)"])
    expect(what).toEqual jasmine.any Array
    expect(what.every (s) -> typeof s is 'string').toBe true

    assertion 'on.dom(["css? script:not([src])", "xpath string(.)"]) => "js…"', ->
    what = `on`.dom(["css? script:not([src])", "xpath string(.)"])
    expect(typeof what).toBe 'string'
    desc = 'Code of first inline script tag'
    console.log "#{desc}:\n#{what}\n(#{desc} ends.)"

    assertion 'on.dom(["css? script:not([src])", "xpath! string(@src)"])' +
    ' => undefined (empty string is not truthy => not a match)', ->
    what = `on`.dom(["css? script:not([src])", "xpath! string(@src)"])
    expect(what).toBe undefined

    assertion 'on.dom(["xpath /svg", "css* *"]) => undefined (not an svg doc)', ->
    expect(`on`.dom(["xpath /svg", "css* *"])).toBe undefined
  14. @johan johan revised this gist Nov 24, 2012. 1 changed file with 13 additions and 3 deletions.
    16 changes: 13 additions & 3 deletions tests/on.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,8 @@
    describe 'on()', ->
    it 'on should be a function', ->
    expect(typeof `on`).toBe 'function'
    fn = `on`

    it 'should throw an error on no input', ->
    try `on()` catch e then err = e
    try fn() catch e then err = e
    expect(err).toNotBe undefined

    it 'should expose an on.dom function after the first call', ->
    @@ -12,6 +11,15 @@ describe 'on()', ->
    it 'should expose an on.query function after the first call', ->
    expect(typeof `on`.query).toBe 'function'

    it 'should expose an on.path_re function after the first call', ->
    expect(typeof `on`.path_re).toBe 'function'

    it 'should accept an object with "path_re", "dom", and/or "query" specs', ->
    what = fn( ready: (->), path_re: '/', dom: 'css *', query: true)
    expect(what).toEqual jasmine.any Array
    expect(what.length).toBeGreaterThan 2



    describe 'on.dom(dom spec – see below for the three types of dom spec)', ->
    it 'should be a function after the first on() call', ->
    @@ -25,6 +33,7 @@ describe 'on.dom(dom spec – see below for the three types of dom spec)', ->
    expect(fns).toBe 'css*,css+,css?,css,xpath*,xpath+,xpath?,xpath!,xpath'



    describe 'on.dom(dom spec type 1: a selector string)', ->
    root = document.documentElement
    assertion = it
    @@ -139,6 +148,7 @@ describe 'on.dom(dom spec type 1: a selector string)', ->
    expect(`on`.dom('xpath! name(/*) = \'nope\'')).toBe undefined



    describe 'on.dom(dom spec type 2: an object showing the structure you want)', ->
    html = document.documentElement
    head = document.querySelector 'head'
  15. @johan johan revised this gist Nov 24, 2012. 1 changed file with 63 additions and 2 deletions.
    65 changes: 63 additions & 2 deletions tests/on.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -9,8 +9,11 @@ describe 'on()', ->
    it 'should expose an on.dom function after the first call', ->
    expect(typeof `on`.dom).toBe 'function'

    it 'should expose an on.query function after the first call', ->
    expect(typeof `on`.query).toBe 'function'

    describe 'on.dom', ->

    describe 'on.dom(dom spec – see below for the three types of dom spec)', ->
    it 'should be a function after the first on() call', ->
    try `on()` catch e then err = e
    expect(err).toNotBe undefined
    @@ -22,7 +25,7 @@ describe 'on.dom', ->
    expect(fns).toBe 'css*,css+,css?,css,xpath*,xpath+,xpath?,xpath!,xpath'


    describe 'on.dom(a selector string)', ->
    describe 'on.dom(dom spec type 1: a selector string)', ->
    root = document.documentElement
    assertion = it

    @@ -134,3 +137,61 @@ describe 'on.dom(a selector string)', ->

    assertion 'on.dom("xpath! name(/*) = \'nope\'") => undefined', ->
    expect(`on`.dom('xpath! name(/*) = \'nope\'')).toBe undefined


    describe 'on.dom(dom spec type 2: an object showing the structure you want)', ->
    html = document.documentElement
    head = document.querySelector 'head'
    try `on()` # ensures there's an on.dom to call
    assertion = it

    pluralize = (n, noun) -> "#{n} #{noun}#{if n is 1 then '' else 's'}"

    assertion 'on.dom({}) => {} (fairly useless, but minimal, test case)', ->
    expect(`on`.dom({})).toEqual {}

    assertion 'on.dom({ h:"css head", H:"css html" }) => { h:head, H:html }', ->
    expect(`on`.dom({ h:"css head", H:"css html" })).toEqual { h:head, H:html }

    assertion 'on.dom({ h:"css head", f:"css? foot" }) => { h:head, f:null }', ->
    expect(`on`.dom({ h:"css head", f:"css? foot" })).toEqual { h:head, f:null }

    assertion 'on.dom({ h:"css head", f:"css foot" }) => undefined (no foot!)', ->
    expect(`on`.dom({ h:"css head", f:"css foot" })).toEqual undefined

    assertion 'on.dom({ x:"css* frame" }) => { x:[] } (frames optional here)', ->
    expect(`on`.dom({ x:"css* frame" })).toEqual { x:[] }

    assertion 'on.dom({ x:"css+ frame" }) => undefined (but mandatory here!)', ->
    expect(`on`.dom({ x:"css+ frame" })).toBe undefined

    assertion 'on.dom({ x:"css* script" }) => { x:[…all (>=0) script tags…] }', ->
    what = `on`.dom({ x:"css* script" })
    expect(what.x).toEqual jasmine.any Array
    expect(what.x.every (s) -> s.nodeName is 'script')

    assertion 'on.dom({ x:"css+ script" }) => { x:[…all (>0) script tags…] }', ->
    what = `on`.dom({ x:"css+ script" })
    expect(what.x).toEqual jasmine.any Array
    expect(what.x.length).toBeGreaterThan 0
    expect(what.x.every (s) -> s.nodeName is 'script')

    assertion 'on.dom({ c:"xpath count(//script)" }) => {c:N} (any N is okay)', ->
    what = `on`.dom({ c:"xpath count(//script)" })
    expect(what).toEqual jasmine.any Object
    expect(N = what.c).toEqual jasmine.any Number
    console.log "on.dom({ c: count(…) }) found #{pluralize N, 'script'}"
    delete what.c
    expect(what).toEqual {}

    assertion 'on.dom({ c:"xpath! count(//script)" }) => {c:N} (only N!=0 ok)', ->
    what = `on`.dom({ c:"xpath! count(//script)" })
    expect(what.c).toBeGreaterThan 0
    delete what.c
    expect(what).toEqual {}

    assertion 'on.dom({ c:"xpath! count(//missing)" }) => undefined (as N==0)', ->
    expect(`on`.dom({ c:"xpath! count(//missing)" })).toBe undefined

    assertion 'on.dom({ c:"xpath! count(//*) and /html" }) => { c:true }', ->
    expect(`on`.dom({ c:"xpath! count(//*) > 5 and /html" })).toEqual c: true
  16. @johan johan revised this gist Nov 24, 2012. 6 changed files with 175 additions and 199 deletions.
    7 changes: 6 additions & 1 deletion testem.json
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,11 @@
    { "framework": "jasmine"
    , "src_files":
    [ "on.js"
    , "tests/*"
    , "tests/*.coffee"
    ]
    , "serve_files":
    [ "on.js"
    , "tests/*.js"
    ]
    , "before_tests": "coffee -c tests/*.coffee"
    }
    1 change: 1 addition & 0 deletions tests/.gitignore
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    *.js
    136 changes: 136 additions & 0 deletions tests/on.dom.coffee
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,136 @@
    describe 'on()', ->
    it 'on should be a function', ->
    expect(typeof `on`).toBe 'function'

    it 'should throw an error on no input', ->
    try `on()` catch e then err = e
    expect(err).toNotBe undefined

    it 'should expose an on.dom function after the first call', ->
    expect(typeof `on`.dom).toBe 'function'


    describe 'on.dom', ->
    it 'should be a function after the first on() call', ->
    try `on()` catch e then err = e
    expect(err).toNotBe undefined

    it 'should expose on.dom.* functions once on.dom() has run once', ->
    try
    `on`.dom()
    fns = Object.keys(`on`.dom).join(',')
    expect(fns).toBe 'css*,css+,css?,css,xpath*,xpath+,xpath?,xpath!,xpath'


    describe 'on.dom(a selector string)', ->
    root = document.documentElement
    assertion = it

    describe 'on.dom("css… selection"): Array/Node, optional/not?', ->
    describe 'Array of Node:s (0+ occurrences):', ->
    assertion 'on.dom("css* NotFound") => []', ->
    expect(`on`.dom('css* NotFound')).toEqual []

    assertion 'on.dom("css* html") => [root element]', ->
    expect(`on`.dom('css* html')).toEqual [root]


    describe 'Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("css+ html") => [root element]', ->
    expect(`on`.dom('css+ html')).toEqual [root]

    assertion 'on.dom("css+ NotFound") => undefined', ->
    expect(`on`.dom('css+ NotFound')).toBe undefined


    describe 'single optional Node, or null if not found:', ->
    assertion 'on.dom("css? *") => root element (= first match)', ->
    expect(`on`.dom('css? *')).toBe root

    assertion 'on.dom("css? NotFound") => null (not found)', ->
    expect(`on`.dom('css? NotFound')).toBe null


    describe 'single mandatory Node:', ->
    assertion 'on.dom("css *") => the root element', ->
    expect(`on`.dom('css *')).toBe root

    assertion 'on.dom("css NotFound") => undefined (unsatisfied)', ->
    expect(`on`.dom('css NotFound')).toBe undefined



    describe 'on.dom("xpath… selection"): Array/Node, optional/not?', ->
    describe 'xpath* => Array of Node:s (0+ occurrences):', ->
    assertion 'on.dom("xpath* /*") => [root element]', ->
    expect(`on`.dom('xpath* /*')).toEqual [root]

    assertion 'on.dom("xpath* /NotFound") => []', ->
    expect(`on`.dom('xpath* /NotFound')).toEqual []


    describe 'xpath+ => Array of Node:s (1+ occurrences):', ->
    assertion 'on.dom("xpath+ /*") => [root element]', ->
    expect(`on`.dom('xpath+ /*')).toEqual [root]

    assertion 'on.dom("xpath+ /NotFound") => undefined', ->
    expect(`on`.dom('xpath+ /NotFound')).toBe undefined


    describe 'xpath? => single optional Node, or null if missing:', ->
    assertion 'on.dom("xpath? /NotFound") => null', ->
    expect(`on`.dom('xpath? /NotFound')).toBe null

    assertion 'on.dom("xpath? /*") => the root element', ->
    expect(`on`.dom('xpath? /*')).toBe root


    describe 'xpath => single mandatory Node:', ->
    assertion 'on.dom("xpath /*") => the root element', ->
    expect(`on`.dom('xpath /*')).toBe root

    assertion 'on.dom("xpath /NotFound") => undefined', ->
    expect(`on`.dom('xpath /NotFound')).toBe undefined

    assertion 'on.dom("xpath .") => the current document', ->
    expect(`on`.dom('xpath .')).toBe document


    describe '…or queries yielding Number/String/Boolean answers:', ->
    assertion 'on.dom("xpath count(/)") => 1', ->
    expect(`on`.dom('xpath count(/)')).toBe 1

    assertion 'on.dom("xpath count(/NotFound)") => 0', ->
    expect(`on`.dom('xpath count(/NotFound)')).toBe 0

    assertion 'on.dom("xpath name(/*)") => "html"', ->
    expect(`on`.dom('xpath name(/*)')).toBe 'html'

    assertion 'on.dom("xpath name(/)") => ""', ->
    expect(`on`.dom('xpath name(/)')).toBe ''

    assertion 'on.dom("xpath name(/*) = \'html\'") => true', ->
    expect(`on`.dom('xpath name(/*) = \'html\'')).toBe true

    assertion 'on.dom("xpath name(/*) = \'nope\'") => false', ->
    expect(`on`.dom('xpath name(/*) = \'nope\'')).toBe false


    describe 'xpath! makes assertions, requiring truthy answers:', ->
    assertion 'on.dom("xpath! count(/)") => 1', ->
    expect(`on`.dom('xpath count(/)')).toBe 1

    assertion 'on.dom("xpath! count(/NotFound)") => undefined', ->
    expect(`on`.dom('xpath! count(/NotFound)')).toBe undefined

    assertion 'on.dom("xpath! name(/*)") => "html"', ->
    expect(`on`.dom('xpath! name(/*)')).toBe 'html'

    assertion 'on.dom("xpath! name(/)") => undefined', ->
    expect(`on`.dom('xpath! name(/)')).toBe undefined

    assertion 'on.dom("xpath! name(/*) = \'html\'") => true', ->
    expect(`on`.dom('xpath! name(/*) = \'html\'')).toBe true

    assertion 'on.dom("xpath! name(/*) = \'nope\'") => undefined', ->
    expect(`on`.dom('xpath! name(/*) = \'nope\'')).toBe undefined
    162 changes: 0 additions & 162 deletions tests/on.dom.js
    Original file line number Diff line number Diff line change
    @@ -1,162 +0,0 @@
    // some tests for test'em:
    // http://net.tutsplus.com/tutorials/javascript-ajax/make-javascript-testing-fun-with-testem/

    describe('on()', function() {
    it('on should be a function', function() {
    expect(typeof on).toBe('function');
    });

    it('should throw an error on no input', function() {
    var res, err;
    try { res = on(); } catch(e) { err = e; }
    expect(err).toNotBe(undefined);
    });

    it('should expose an on.dom function after the first call', function() {
    expect(typeof on.dom).toBe('function');
    });
    });


    describe('on.dom', function() {
    it('should be a function after the first on() call', function() {
    try { on(); } catch(e) {}
    expect(typeof on.dom).toBe('function');
    });

    it('should expose on.dom.* functions once on.dom() has run once', function() {
    try { on.dom(); } catch(e) {}
    var fns = Object.keys(on.dom).join(',');
    expect(fns).toBe('css*,css+,css?,css,xpath*,xpath+,xpath?,xpath!,xpath');
    });
    });


    describe('on.dom(a selector string)', function() {
    var root = document.documentElement, asserts = it;

    describe('on.dom("css… selection"): Array/Node, optional/not?', function() {
    describe('Array of Node:s (0+ occurrences):', function() {
    asserts('on.dom("css* NotFound") => []', function() {
    expect(on.dom("css* NotFound")).toEqual([]);
    });
    asserts('on.dom("css* html") => [root element]', function() {
    expect(on.dom("css* html")).toEqual([root]);
    });
    });

    describe('Array of Node:s (1+ occurrences):', function() {
    asserts('on.dom("css+ html") => [root element]', function() {
    expect(on.dom("css+ html")).toEqual([root]);
    });
    asserts('on.dom("css+ NotFound") => undefined', function() {
    expect(on.dom("css+ NotFound")).toBe(undefined);
    });
    });

    describe('single optional Node, or null if not found:', function() {
    asserts('on.dom("css? *") => root element (= first match)', function() {
    expect(on.dom("css? *")).toBe(root);
    });
    asserts('on.dom("css? NotFound") => null (not found)', function() {
    expect(on.dom("css? NotFound")).toBe(null);
    });
    });

    describe('single mandatory Node:', function() {
    asserts('on.dom("css *") => the root element', function() {
    expect(on.dom("css *")).toBe(root);
    });
    asserts('on.dom("css NotFound") => undefined (unsatisfied)', function() {
    expect(on.dom("css NotFound")).toBe(undefined);
    });
    });
    });

    describe('on.dom("xpath… selection"): Array/Node, optional/not?', function() {
    describe('xpath* => Array of Node:s (0+ occurrences):', function() {
    asserts('on.dom("xpath* /*") => [root element]', function () {
    expect(on.dom("xpath* /*")).toEqual([root]);
    });
    asserts('on.dom("xpath* /NotFound") => []', function () {
    expect(on.dom("xpath* /NotFound")).toEqual([]);
    });
    });

    describe('xpath+ => Array of Node:s (1+ occurrences):', function() {
    asserts('on.dom("xpath+ /*") => [root element]', function () {
    expect(on.dom("xpath+ /*")).toEqual([root]);
    });
    asserts('on.dom("xpath+ /NotFound") => undefined', function () {
    expect(on.dom("xpath+ /NotFound")).toBe(undefined);
    });
    });

    describe('xpath? => single optional Node, or null if missing:', function() {
    asserts('on.dom("xpath? /NotFound") => null', function() {
    expect(on.dom("xpath? /NotFound")).toBe(null);
    });
    asserts('on.dom("xpath? /*") => the root element', function() {
    expect(on.dom("xpath? /*")).toBe(root);
    });
    });

    describe('xpath => single mandatory Node:', function() {
    asserts('on.dom("xpath /*") => the root element', function() {
    expect(on.dom("xpath /*")).toBe(root);
    });
    asserts('on.dom("xpath /NotFound") => undefined', function() {
    expect(on.dom("xpath /NotFound")).toBe(undefined);
    });
    asserts('on.dom("xpath .") => the current document', function() {
    expect(on.dom("xpath .")).toBe(document);
    });
    });

    describe('…or queries yielding Number/String/Boolean answers:', function() {
    asserts('on.dom("xpath count(/)") => 1', function() {
    expect(on.dom("xpath count(/)")).toBe(1);
    });
    asserts('on.dom("xpath count(/NotFound)") => 0', function() {
    expect(on.dom("xpath count(/NotFound)")).toBe(0);
    });

    asserts('on.dom("xpath name(/*)") => "html"', function() {
    expect(on.dom("xpath name(/*)")).toBe('html');
    });
    asserts('on.dom("xpath name(/)") => ""', function() {
    expect(on.dom("xpath name(/)")).toBe('');
    });

    asserts('on.dom("xpath name(/*) = \'html\'") => true', function() {
    expect(on.dom("xpath name(/*) = \'html\'")).toBe(true);
    });
    asserts('on.dom("xpath name(/*) = \'nope\'") => false', function() {
    expect(on.dom("xpath name(/*) = \'nope\'")).toBe(false);
    });
    });

    describe('xpath! makes assertions, requiring truthy answers:', function() {
    asserts('on.dom("xpath! count(/)") => 1', function() {
    expect(on.dom("xpath count(/)")).toBe(1);
    });
    asserts('on.dom("xpath! count(/NotFound)") => undefined', function() {
    expect(on.dom("xpath! count(/NotFound)")).toBe(undefined);
    });

    asserts('on.dom("xpath! name(/*)") => "html"', function() {
    expect(on.dom("xpath! name(/*)")).toBe('html');
    });
    asserts('on.dom("xpath! name(/)") => undefined', function() {
    expect(on.dom("xpath! name(/)")).toBe(undefined);
    });

    asserts('on.dom("xpath! name(/*) = \'html\'") => true', function() {
    expect(on.dom("xpath! name(/*) = \'html\'")).toBe(true);
    });
    asserts('on.dom("xpath! name(/*) = \'nope\'") => undefined', function() {
    expect(on.dom("xpath! name(/*) = \'nope\'")).toBe(undefined);
    });
    });
    });
    });
    32 changes: 32 additions & 0 deletions tests/on.query.coffee
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,32 @@
    describe 'on.query', ->
    q_was = location.search
    query = (q) ->
    if location.search isnt q
    url = location.href.replace /(\?[^#]*)?(#.*)?$/, "#{q}$2"
    history.replaceState history.state, document.title, url

    it 'should be a function after the first on() call', ->
    try `on()`
    expect(typeof `on`.query).toBe 'function'

    it 'on.query() => {} for a missing query string', ->
    query ''
    expect(`on`.query()).toEqual {}

    it 'on.query() => {} for an empty query string ("?")', ->
    query '?'
    expect(`on`.query()).toEqual {}

    it 'on.query() => { a:"", x:"0" } for a query string "?a=&x=0"', ->
    query '?a=&x=0'
    expect(`on`.query()).toEqual
    a: ''
    x: '0'

    it 'on.query() => { ugh:undefined } for a query string "?ugh"', ->
    query '?ugh'
    result = `on`.query()
    expect('ugh' of result).toBe true
    expect(result).toEqual {} # FIXME - better test framework?
    expect(result.ugh).toBe `undefined`
    query q_was # reset, for good measure
    36 changes: 0 additions & 36 deletions tests/on.query.js
    Original file line number Diff line number Diff line change
    @@ -1,36 +0,0 @@
    describe('on.query', function() {
    function query(q) {
    if (location.search !== q) {
    var url = location.href.replace(/(\?[^#]*)?(#.*)?$/, q + '$2');
    history.replaceState(history.state, document.title, url);
    }
    }

    it('should be a function after the first on() call', function() {
    try { on(); } catch(e) {}
    expect(typeof on.query).toBe('function');
    });

    it('on.query() => {} for a missing query string', function() {
    query('');
    expect(on.query()).toEqual({});
    });

    it('on.query() => {} for an empty query string ("?")', function() {
    query('?');
    expect(on.query()).toEqual({});
    });

    it('on.query() => { a:"", x:"0" } for a query string "?a=&x=0"', function() {
    query('?a=&x=0');
    expect(on.query()).toEqual({ a:"", x:"0" });
    });

    it('on.query() => { ugh:undefined } for a query string "?ugh"', function() {
    query('?ugh');
    var result = on.query();
    expect('ugh' in result).toBe(true);
    expect(result).toEqual({}); // FIXME - better test framework?
    expect(result.ugh).toBe(undefined);
    });
    });
  17. @johan johan revised this gist Nov 24, 2012. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -336,8 +336,8 @@ function on(opts) {
    if (isArray(context)) {
    for (results = [], i = 0; i < context.length; i++) {
    result = test_dom.call(context[i], spec);
    if (result === FAIL) return FAIL;
    results.push(result);
    if (result !== FAIL)
    results.push(result);
    }
    return results;
    }
  18. @johan johan revised this gist Nov 24, 2012. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -348,6 +348,7 @@ function on(opts) {
    if (typeof spec === 'string') return lookup.call(context, spec);
    if (isArray(spec)) {
    context = lookup.call(context, spec[0]);
    if (context === null || context === FAIL) return context;
    return test_dom.call(context, spec[1]);
    }
    if (isObject(spec)) {
  19. @johan johan revised this gist Nov 24, 2012. 1 changed file with 9 additions and 1 deletion.
    10 changes: 9 additions & 1 deletion on.js
    Original file line number Diff line number Diff line change
    @@ -145,6 +145,7 @@ function on(opts) {
    , load = get('load')
    , pushState = get('pushstate')
    , pjax_event = get('pjaxevent')
    , parse_dom_rule // regexp on.dom uses to recognize its various sub-commands
    , name, rule, test, result, retry
    ;

    @@ -298,17 +299,24 @@ function on(opts) {
    return got instanceof Array ? got[0] || null : got;
    }

    function quoteRe(s) { return (s+'').replace(/([-$(-+.?[-^{|}])/g, '\\$1'); }

    // DOM constraint tester / scraper facility:
    // "this" is the context Node(s) - initially the document
    // "spec" is either of:
    // * css / xpath Selector "selector_type selector"
    // * resolved for context [ context Selector, spec ]
    // * an Object of spec(s) { property_name: spec, ... }
    function test_dom(spec, context) {
    // returns FAIL if it turned out it wasn't a mandated match at this level
    // returns null if it didn't find optional matches at this level
    // returns Node or an Array of nodes, or a basic type from some XPath query
    function lookup(rule) {
    if (typeof rule !== 'string')
    throw new Error('non-String dom match rule: '+ rule);
    var match = /^((?:css|xpath)[?+*!]?)\s+(.*)/.exec(rule), type, func;
    if (!parse_dom_rule) parse_dom_rule = new RegExp('^(' +
    Object.keys(on.dom).map(quoteRe).join('|') + ')\\s*(.*)');
    var match = parse_dom_rule.exec(rule), type, func;
    if (match) {
    type = match[1];
    rule = match[2];
  20. @johan johan revised this gist Nov 24, 2012. 2 changed files with 11 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -241,11 +241,12 @@ function on(opts) {
    function unparam(query) {
    var data = {};
    (query || '').replace(/\+/g, '%20').split('&').forEach(function(kv) {
    kv = /^\??([^=]*)=(.*)/.exec(kv);
    kv = /^\??([^=&]*)(?:=(.*))?/.exec(kv);
    if (!kv) return;
    var prop, val, k = kv[1], v = kv[2], e, m;
    try { prop = decodeURIComponent(k); } catch (e) { prop = unescape(k); }
    try { val = decodeURIComponent(v); } catch (e) { val = unescape(v); }
    if ((val = v) != null)
    try { val = decodeURIComponent(v); } catch (e) { val = unescape(v); }
    data[prop] = val;
    });
    return data;
    8 changes: 8 additions & 0 deletions tests/on.query.js
    Original file line number Diff line number Diff line change
    @@ -25,4 +25,12 @@ describe('on.query', function() {
    query('?a=&x=0');
    expect(on.query()).toEqual({ a:"", x:"0" });
    });

    it('on.query() => { ugh:undefined } for a query string "?ugh"', function() {
    query('?ugh');
    var result = on.query();
    expect('ugh' in result).toBe(true);
    expect(result).toEqual({}); // FIXME - better test framework?
    expect(result.ugh).toBe(undefined);
    });
    });
  21. @johan johan revised this gist Nov 24, 2012. 1 changed file with 28 additions and 0 deletions.
    28 changes: 28 additions & 0 deletions tests/on.query.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    describe('on.query', function() {
    function query(q) {
    if (location.search !== q) {
    var url = location.href.replace(/(\?[^#]*)?(#.*)?$/, q + '$2');
    history.replaceState(history.state, document.title, url);
    }
    }

    it('should be a function after the first on() call', function() {
    try { on(); } catch(e) {}
    expect(typeof on.query).toBe('function');
    });

    it('on.query() => {} for a missing query string', function() {
    query('');
    expect(on.query()).toEqual({});
    });

    it('on.query() => {} for an empty query string ("?")', function() {
    query('?');
    expect(on.query()).toEqual({});
    });

    it('on.query() => { a:"", x:"0" } for a query string "?a=&x=0"', function() {
    query('?a=&x=0');
    expect(on.query()).toEqual({ a:"", x:"0" });
    });
    });
  22. @johan johan revised this gist Nov 24, 2012. 2 changed files with 21 additions and 13 deletions.
    28 changes: 15 additions & 13 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -108,18 +108,18 @@ function on(opts) {
    , Array_slice = Array.prototype.slice
    , FAIL = 'dom' in on ? undefined : (function() {
    var tests =
    { path_re: { fn: test_regexp, self: location.pathname }
    , query: { fn: test_query, self: location.search }
    , dom: { fn: test_dom, self: document
    , my: { 'css': not_null($C)
    , 'css?': $C
    { path_re: { fn: test_regexp }
    , query: { fn: test_query }
    , dom: { fn: test_dom
    , my: { 'css*': $c
    , 'css+': one_or_more($c)
    , 'css*': $c
    , 'xpath': not_null($X)
    , 'xpath?': $X
    , 'xpath+': one_or_more($x)
    , 'css?': $C
    , 'css': not_null($C)
    , 'xpath*': $x
    , 'xpath+': one_or_more($x)
    , 'xpath?': $X
    , 'xpath!': truthy($x)
    , 'xpath': not_null($X)
    }
    }
    , inject: { fn: inject }
    @@ -133,7 +133,7 @@ function on(opts) {
    if ((my = test.my))
    for (mine in my)
    me[mine] = my[mine];
    on[name] = me.bind(test.self);
    on[name] = me;
    }
    })()

    @@ -233,7 +233,7 @@ function on(opts) {
    }

    function test_query(spec) {
    var q = unparam(this);
    var q = unparam(this === on || this === window ? location.search : this);
    if (spec === true || spec == null) return q; // decode the query for me!
    throw new Error('bad query type '+ (typeof spec) +': '+ spec);
    }
    @@ -258,7 +258,7 @@ function on(opts) {
    if (!(re instanceof RegExp))
    throw new Error((typeof re) +' was not a regexp: '+ re);

    var ok = re.exec(this);
    var ok = re.exec(this === on || this === window ? location.pathname : this);
    if (ok === null) return FAIL;
    if (!spec.length) return ok;
    var named = {};
    @@ -318,7 +318,9 @@ function on(opts) {
    }

    var results, result, i, property_name;
    if (context === undefined) context = this;
    if (context === undefined) {
    context = this === on || this === window ? document : this;
    }

    // validate context:
    if (context === null || context === FAIL) return FAIL;
    6 changes: 6 additions & 0 deletions tests/on.dom.js
    Original file line number Diff line number Diff line change
    @@ -23,6 +23,12 @@ describe('on.dom', function() {
    try { on(); } catch(e) {}
    expect(typeof on.dom).toBe('function');
    });

    it('should expose on.dom.* functions once on.dom() has run once', function() {
    try { on.dom(); } catch(e) {}
    var fns = Object.keys(on.dom).join(',');
    expect(fns).toBe('css*,css+,css?,css,xpath*,xpath+,xpath?,xpath!,xpath');
    });
    });


  23. @johan johan revised this gist Nov 24, 2012. 2 changed files with 162 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions testem.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    { "framework": "jasmine"
    , "src_files":
    [ "on.js"
    , "tests/*"
    ]
    }
    156 changes: 156 additions & 0 deletions tests/on.dom.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,156 @@
    // some tests for test'em:
    // http://net.tutsplus.com/tutorials/javascript-ajax/make-javascript-testing-fun-with-testem/

    describe('on()', function() {
    it('on should be a function', function() {
    expect(typeof on).toBe('function');
    });

    it('should throw an error on no input', function() {
    var res, err;
    try { res = on(); } catch(e) { err = e; }
    expect(err).toNotBe(undefined);
    });

    it('should expose an on.dom function after the first call', function() {
    expect(typeof on.dom).toBe('function');
    });
    });


    describe('on.dom', function() {
    it('should be a function after the first on() call', function() {
    try { on(); } catch(e) {}
    expect(typeof on.dom).toBe('function');
    });
    });


    describe('on.dom(a selector string)', function() {
    var root = document.documentElement, asserts = it;

    describe('on.dom("css… selection"): Array/Node, optional/not?', function() {
    describe('Array of Node:s (0+ occurrences):', function() {
    asserts('on.dom("css* NotFound") => []', function() {
    expect(on.dom("css* NotFound")).toEqual([]);
    });
    asserts('on.dom("css* html") => [root element]', function() {
    expect(on.dom("css* html")).toEqual([root]);
    });
    });

    describe('Array of Node:s (1+ occurrences):', function() {
    asserts('on.dom("css+ html") => [root element]', function() {
    expect(on.dom("css+ html")).toEqual([root]);
    });
    asserts('on.dom("css+ NotFound") => undefined', function() {
    expect(on.dom("css+ NotFound")).toBe(undefined);
    });
    });

    describe('single optional Node, or null if not found:', function() {
    asserts('on.dom("css? *") => root element (= first match)', function() {
    expect(on.dom("css? *")).toBe(root);
    });
    asserts('on.dom("css? NotFound") => null (not found)', function() {
    expect(on.dom("css? NotFound")).toBe(null);
    });
    });

    describe('single mandatory Node:', function() {
    asserts('on.dom("css *") => the root element', function() {
    expect(on.dom("css *")).toBe(root);
    });
    asserts('on.dom("css NotFound") => undefined (unsatisfied)', function() {
    expect(on.dom("css NotFound")).toBe(undefined);
    });
    });
    });

    describe('on.dom("xpath… selection"): Array/Node, optional/not?', function() {
    describe('xpath* => Array of Node:s (0+ occurrences):', function() {
    asserts('on.dom("xpath* /*") => [root element]', function () {
    expect(on.dom("xpath* /*")).toEqual([root]);
    });
    asserts('on.dom("xpath* /NotFound") => []', function () {
    expect(on.dom("xpath* /NotFound")).toEqual([]);
    });
    });

    describe('xpath+ => Array of Node:s (1+ occurrences):', function() {
    asserts('on.dom("xpath+ /*") => [root element]', function () {
    expect(on.dom("xpath+ /*")).toEqual([root]);
    });
    asserts('on.dom("xpath+ /NotFound") => undefined', function () {
    expect(on.dom("xpath+ /NotFound")).toBe(undefined);
    });
    });

    describe('xpath? => single optional Node, or null if missing:', function() {
    asserts('on.dom("xpath? /NotFound") => null', function() {
    expect(on.dom("xpath? /NotFound")).toBe(null);
    });
    asserts('on.dom("xpath? /*") => the root element', function() {
    expect(on.dom("xpath? /*")).toBe(root);
    });
    });

    describe('xpath => single mandatory Node:', function() {
    asserts('on.dom("xpath /*") => the root element', function() {
    expect(on.dom("xpath /*")).toBe(root);
    });
    asserts('on.dom("xpath /NotFound") => undefined', function() {
    expect(on.dom("xpath /NotFound")).toBe(undefined);
    });
    asserts('on.dom("xpath .") => the current document', function() {
    expect(on.dom("xpath .")).toBe(document);
    });
    });

    describe('…or queries yielding Number/String/Boolean answers:', function() {
    asserts('on.dom("xpath count(/)") => 1', function() {
    expect(on.dom("xpath count(/)")).toBe(1);
    });
    asserts('on.dom("xpath count(/NotFound)") => 0', function() {
    expect(on.dom("xpath count(/NotFound)")).toBe(0);
    });

    asserts('on.dom("xpath name(/*)") => "html"', function() {
    expect(on.dom("xpath name(/*)")).toBe('html');
    });
    asserts('on.dom("xpath name(/)") => ""', function() {
    expect(on.dom("xpath name(/)")).toBe('');
    });

    asserts('on.dom("xpath name(/*) = \'html\'") => true', function() {
    expect(on.dom("xpath name(/*) = \'html\'")).toBe(true);
    });
    asserts('on.dom("xpath name(/*) = \'nope\'") => false', function() {
    expect(on.dom("xpath name(/*) = \'nope\'")).toBe(false);
    });
    });

    describe('xpath! makes assertions, requiring truthy answers:', function() {
    asserts('on.dom("xpath! count(/)") => 1', function() {
    expect(on.dom("xpath count(/)")).toBe(1);
    });
    asserts('on.dom("xpath! count(/NotFound)") => undefined', function() {
    expect(on.dom("xpath! count(/NotFound)")).toBe(undefined);
    });

    asserts('on.dom("xpath! name(/*)") => "html"', function() {
    expect(on.dom("xpath! name(/*)")).toBe('html');
    });
    asserts('on.dom("xpath! name(/)") => undefined', function() {
    expect(on.dom("xpath! name(/)")).toBe(undefined);
    });

    asserts('on.dom("xpath! name(/*) = \'html\'") => true', function() {
    expect(on.dom("xpath! name(/*) = \'html\'")).toBe(true);
    });
    asserts('on.dom("xpath! name(/*) = \'nope\'") => undefined', function() {
    expect(on.dom("xpath! name(/*) = \'nope\'")).toBe(undefined);
    });
    });
    });
    });
  24. @johan johan revised this gist Nov 7, 2012. 2 changed files with 73 additions and 11 deletions.
    77 changes: 72 additions & 5 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -43,7 +43,7 @@
    (see http://goo.gl/ejtMD for a more thorough discussion of something similar)
    The dom prtoperty is recursively defined so you can make nested structures.
    The dom property is recursively defined so you can make nested structures.
    If you want a property that itself is an object full of matched things, pass
    an object of sub-dom-spec:s, instead of a string selector:
    @@ -79,6 +79,12 @@
    that is not found will instead result in that part of your DOM being null, or
    an empty array, in the case of a * selector.
    Finally, there is the xpath! keyword, which is similar to xpath, but it also
    mandates that whatever is returned is truthy. This is useful when you use the
    xpath functions returning strings, numbers and of course booleans, to assert
    things about the pages you want to run on, like 'xpath! count(//img) = 0', if
    you never want the script to run on pages with inline images, say.
    After you have called on(), you may call on.dom to do page scraping later on,
    returning whatever matched your selector(s) passed. Mandatory selectors which
    failed to match at this point will return undefined, optional selectors null:
    @@ -90,6 +96,11 @@
    A readable way to detect a failed mandatory match is on.dom(...) === on.FAIL;
    Github pjax hook: for re-running a script's on() block for every pjax request
    to a site - add a pushstate hook as per http://goo.gl/LNSv1 -- and be sure to
    make your script reentrant, so that it won't try to process the same elements
    again, if they are still sitting around in the page (see ':not([augmented])')
    */

    function on(opts) {
    @@ -111,6 +122,7 @@ function on(opts) {
    , 'xpath!': truthy($x)
    }
    }
    , inject: { fn: inject }
    }
    , name, test, me, my, mine
    ;
    @@ -131,11 +143,23 @@ function on(opts) {
    , script = get('name')
    , ready = get('ready')
    , load = get('load')
    , name, rule, test, result
    , pushState = get('pushstate')
    , pjax_event = get('pjaxevent')
    , name, rule, test, result, retry
    ;

    if (typeof ready !== 'function' && typeof load !== 'function')
    if (typeof ready !== 'function' &&
    typeof load !== 'function' &&
    typeof pushState !== 'function') {
    alert('no on function');
    throw new Error('on() needs at least a "ready" or "load" function!');
    }

    if (pushState && history.pushState &&
    (on.pushState = on.pushState || []).indexOf(opts) === -1) {
    on.pushState.push(opts); // make sure we don't re-register after navigation
    initPushState(pushState, pjax_event);
    }

    try {
    for (name in rules) {
    @@ -153,9 +177,11 @@ function on(opts) {
    return false;
    }

    if (ready) ready.apply(opts, input.concat());
    if (load) window.addEventListener('load', function() {
    if (ready) {
    ready.apply(opts, input.concat());
    }
    if (load) window.addEventListener('load', function() {
    load.apply(opts, input.concat());
    });
    return input.concat(opts);

    @@ -164,6 +190,47 @@ function on(opts) {
    function isObject(x) { return Object_toString.call(x) === '[object Object]'; }
    function array(a) { return Array_slice.call(a, 0); } // array:ish => Array
    function arrayify(x) { return isArray(x) ? x : [x]; } // non-array? => Array
    function inject(fn, args) {
    var script = document.createElement('script')
    , parent = document.documentElement;
    args = JSON.stringify(args || []).slice(1, -1);
    script.textContent = '('+ fn +')('+ args +');';
    parent.appendChild(script);
    parent.removeChild(script);
    }

    function initPushState(callback, pjax_event) {
    if (!history.pushState.armed) {
    inject(function(pjax_event) {
    function reportBack() {
    var e = document.createEvent('Events');
    e.initEvent('history.pushState', !'bubbles', !'cancelable');
    document.dispatchEvent(e);
    }
    var pushState = history.pushState;
    history.pushState = function on_pushState() {
    if (pjax_event && window.$ && $.pjax)
    $(document).one(pjax_event, reportBack);
    else
    setTimeout(reportBack, 0);
    return pushState.apply(this, arguments);
    };
    }, [pjax_event]);
    history.pushState.armed = pjax_event;
    }

    retry = function after_pushState() {
    rules = Object.create(opts);
    rules.load = rules.pushstate = undefined;
    rules.ready = callback;
    on(rules);
    };

    document.addEventListener('history.pushState', function() {
    if (debug) console.log('on.pushstate', location.pathname);
    retry();
    }, false);
    }

    function test_query(spec) {
    var q = unparam(this);
    7 changes: 1 addition & 6 deletions on.min.js
    Original file line number Diff line number Diff line change
    @@ -1,6 +1 @@
    function on(h){function l(a){v[a]=void 0;return h[a]}function m(a){return"[object Array]"===z.call(a)}function F(a){var b={};(this||"").replace(/\+/g,"%20").split("&").forEach(function(a){if(a=/^\??([^=]*)=(.*)/.exec(a)){var c,f,g=a[1],a=a[2];try{c=decodeURIComponent(g)}catch(N){c=unescape(g)}try{f=decodeURIComponent(a)}catch(d){f=unescape(a)}b[c]=f}});if(!0===a||null==a)return b;throw Error("bad query type "+typeof a+": "+a);}function G(a){m(a)||(a=m(a)?a:[a]);var b=a.shift();"string"===typeof b&&
    (b=RegExp(b));if(!(b instanceof RegExp))throw Error(typeof b+" was not a regexp: "+b);b=b.exec(this);if(null===b)return c;if(!a.length)return b;var e={};for(b.shift();a.length;)e[a.shift()]=b.shift();return e}function A(a){return function(b){var e=a.apply(this,arguments);return null!==e?e:c}}function B(a){return function(b){var e=a.apply(this,arguments);return e.length?e:c}}function C(a){a=this.querySelectorAll(a);return H.call(a,0)}function D(a){return this.querySelector(a)}function n(a){var b=(this.evaluate?
    this:this.ownerDocument).evaluate(a,this,null,0,null),c=[];switch(b.resultType){case b.STRING_TYPE:return b.stringValue;case b.NUMBER_TYPE:return b.numberValue;case b.BOOLEAN_TYPE:return b.booleanValue;default:for(;a=b.iterateNext();)c.push(a);return c}}function E(a){a=n.call(this,a);return a instanceof Array?a[0]||null:a}function i(a,b){function e(a){if("string"!==typeof a)throw Error("non-String dom match rule: "+a);var b=/^((?:css|xpath)[?+*!]?)\s+(.*)/.exec(a),c,d;b&&(c=b[1],a=b[2],d=i[c]);if(!d)throw Error("unknown dom match rule "+
    c+": "+a);return d.call(this,a)}var d,f,g;void 0===b&&(b=this);if(null===b||b===c)return c;if(m(b)){d=[];for(g=0;g<b.length;g++){f=i.call(b[g],a);if(f===c)return c;d.push(f)}return d}if("object"!==typeof b||!("nodeType"in b))throw Error("illegal context: "+b);if("string"===typeof a)return e.call(b,a);if(m(a))return b=e.call(b,a[0]),i.call(b,a[1]);if("[object Object]"===z.call(a)){d={};for(g in a){f=i.call(b,a[g]);if(f===c)return c;d[g]=f}return d}throw Error("dom spec was neither a String, Object nor Array: "+
    a);}var z=Object.prototype.toString,H=Array.prototype.slice;if(!("dom"in on)){var p={fn:G,self:location.pathname},j={fn:F,self:location.search},q=i,r=A(D),I=B(C),J=A(E),K=B(n),L=n,p={path_re:p,query:j,dom:{fn:q,self:document,my:{css:r,"css?":D,"css+":I,"css*":C,xpath:J,"xpath?":E,"xpath+":K,"xpath*":n,"xpath!":function(a){return L.apply(this,arguments)||c}}}},k,d;for(k in p){j=p[k];q=j.fn;if(r=j.my)for(d in r)q[d]=r[d];on[k]=q.bind(j.self)}}var c=void 0,s=[],v=Object.create(h);k=l("debug");l("name");
    var t=l("ready");d=l("load");var u,w,x,y;if("function"!==typeof t&&"function"!==typeof d)throw Error('on() needs at least a "ready" or "load" function!');try{for(u in v)if(w=v[u],void 0!==w){x=on[u];if(!x)throw Error('did not grok rule "'+u+'"!');y=x(w);if(y===c)return!1;s.push(y)}}catch(M){return k&&console.warn("on(debug): we didn't run because "+M.message),!1}t&&t.apply(h,s.concat());d&&window.addEventListener("load",function(){t.apply(h,s.concat())});return s.concat(h)};
    function on(d){function j(a){h[a]=void 0;return d[a]}function n(a){return"[object Array]"===A.call(a)}function B(a,b){var c=document.createElement("script"),f=document.documentElement,b=JSON.stringify(b||[]).slice(1,-1);c.textContent="("+a+")("+b+");";f.appendChild(c);f.removeChild(c)}function J(a){var b={};(this||"").replace(/\+/g,"%20").split("&").forEach(function(a){if(a=/^\??([^=]*)=(.*)/.exec(a)){var f,e,g=a[1],a=a[2];try{f=decodeURIComponent(g)}catch(d){f=unescape(g)}try{e=decodeURIComponent(a)}catch(h){e=unescape(a)}b[f]=e}});if(!0===a||null==a)return b;throw Error("bad query type "+typeof a+": "+a);}function K(a){n(a)||(a=n(a)?a:[a]);var b=a.shift();"string"===typeof b&&(b=RegExp(b));if(!(b instanceof RegExp))throw Error(typeof b+" was not a regexp: "+b);b=b.exec(this);if(null===b)return e;if(!a.length)return b;var c={};for(b.shift();a.length;)c[a.shift()]=b.shift();return c}function C(a){return function(b){var c=a.apply(this,arguments);return null!==c?c:e}}function D(a){return function(b){var c=a.apply(this,arguments);return c.length?c:e}}function E(a){a=this.querySelectorAll(a);return L.call(a,0)}function F(a){return this.querySelector(a)}function p(a){var b=(this.evaluate?this:this.ownerDocument).evaluate(a,this,null,0,null),c=[];switch(b.resultType){case b.STRING_TYPE:return b.stringValue;case b.NUMBER_TYPE:return b.numberValue;case b.BOOLEAN_TYPE:return b.booleanValue;default:for(;a=b.iterateNext();)c.push(a);return c}}function G(a){a=p.call(this,a);return a instanceof Array?a[0]||null:a}function l(a,b){function c(a){if("string"!==typeof a)throw Error("non-String dom match rule: "+a);var b=/^((?:css|xpath)[?+*!]?)\s+(.*)/.exec(a),c,d;b&&(c=b[1],a=b[2],d=l[c]);if(!d)throw Error("unknown dom match rule "+c+": "+a);return d.call(this,a)}var f,d,g;void 0===b&&(b=this);if(null===b||b===e)return e;if(n(b)){f=[];for(g=0;g<b.length;g++){d=l.call(b[g],a);if(d===e)return e;f.push(d)}return f}if("object"!==typeof b||!("nodeType"in b))throw Error("illegal context: "+b);if("string"===typeof a)return c.call(b,a);if(n(a))return b=c.call(b,a[0]),l.call(b,a[1]);if("[object Object]"===A.call(a)){f={};for(g in a){d=l.call(b,a[g]);if(d===e)return e;f[g]=d}return f}throw Error("dom spec was neither a String, Object nor Array: "+a);}var A=Object.prototype.toString,L=Array.prototype.slice;if(!("dom"in on)){var q={fn:K,self:location.pathname},m={fn:J,self:location.search},r=l,s=C(F),M=D(E),N=C(G),O=D(p),P=p,q={path_re:q,query:m,dom:{fn:r,self:document,my:{css:s,"css?":F,"css+":M,"css*":E,xpath:N,"xpath?":G,"xpath+":O,"xpath*":p,"xpath!":function(a){return P.apply(this,arguments)||e}}},inject:{fn:B}},i,k;for(i in q){m=q[i];r=m.fn;if(s=m.my)for(k in s)r[k]=s[k];on[i]=r.bind(m.self)}}var e=void 0,t=[],h=Object.create(d),H=j("debug");j("name");i=j("ready");var v=j("load"),w=j("pushstate");k=j("pjaxevent");var u,x,y,z,I;if("function"!==typeof i&&"function"!==typeof v&&"function"!==typeof w)throw alert("no on function"),Error('on() needs at least a "ready" or "load" function!');if(w&&history.pushState&&-1===(on.pushState=on.pushState||[]).indexOf(d))on.pushState.push(d),history.pushState.armed||(B(function(a){function b(){var a=document.createEvent("Events");a.initEvent("history.pushState",!1,!1);document.dispatchEvent(a)}var c=history.pushState;history.pushState=function(){if(a&&window.$&&$.pjax)$(document).one(a,b);else setTimeout(b,0);return c.apply(this,arguments)}},[k]),history.pushState.armed=k),I=function(){h=Object.create(d);h.load=h.pushstate=void 0;h.ready=w;on(h)},document.addEventListener("history.pushState",function(){H&&console.log("on.pushstate",location.pathname);I()},!1);try{for(u in h)if(x=h[u],void 0!==x){y=on[u];if(!y)throw Error('did not grok rule "'+u+'"!');z=y(x);if(z===e)return!1;t.push(z)}}catch(Q){return H&&console.warn("on(debug): we didn't run because "+Q.message),!1}i&&i.apply(d,t.concat());v&&window.addEventListener("load",function(){v.apply(d,t.concat())});return t.concat(d)};
  25. @johan johan created this gist Oct 26, 2012.
    287 changes: 287 additions & 0 deletions on.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,287 @@
    /* coffee-script example usage - at https://github.com/johan/dotjs/commits/johan
    on path_re: ['^/([^/]+)/([^/]+)(/?.*)', 'user', 'repo', 'rest']
    query: true
    dom:
    keyboard: 'css .keyboard-shortcuts'
    branches: 'css+ .js-filter-branches h4 a'
    dates: 'css* .commit-group-heading'
    tracker: 'css? #gauges-tracker[defer]'
    johan_ci: 'xpath* //li[contains(@class,"commit")][.//a[.="johan"]]'
    ready: (path, query, dom) ->
    ...would make something like this call, as the path regexp matched, and there
    were DOM matches for the two mandatory "keyboard" and "branches" selectors:
    ready( { user: 'johan', repo: 'dotjs', rest: '/commits/johan' }
    , {} // would contain all query args (if any were present)
    , { keyboard: Node<a href="#keyboard_shortcuts_pane">
    , branches: [ Node<a href="/johan/dotjs/commits/coffee">
    , Node<a href="/johan/dotjs/commits/dirs">
    , Node<a href="/johan/dotjs/commits/gh-pages">
    , Node<a href="/johan/dotjs/commits/johan">
    , Node<a href="/johan/dotjs/commits/jquery-1.8.2">
    , Node<a href="/johan/dotjs/commits/master">
    ]
    , dates: [ Node<h3 class="commit-group-heading">Oct 07, 2012</h3>
    , Node<h3 class="commit-group-heading">Aug 29, 2012</h3>
    , ...
    ]
    , tracker: null
    , johan_ci: [ Node<li class="commit">, ... ]
    }
    )
    A selector returns an array of matches prefixed for "css*" and "css+" (ditto
    xpath), and a single result if it is prefixed "css" or "css?":
    If your script should only run on pages with a particular DOM node (or set of
    nodes), use the 'css' or 'css+' (ditto xpath) forms - and your callback won't
    get fired on pages that lack them. The 'css?' and 'css*' forms would run your
    callback but pass null or [] respectively, on not finding such nodes. You may
    recognize the semantics of x, x?, x* and x+ from regular expressions.
    (see http://goo.gl/ejtMD for a more thorough discussion of something similar)
    The dom prtoperty is recursively defined so you can make nested structures.
    If you want a property that itself is an object full of matched things, pass
    an object of sub-dom-spec:s, instead of a string selector:
    on dom:
    meta:
    base: 'xpath? /head/base
    title: 'xpath string(/head/title)'
    commits: 'css* li.commit'
    ready: (dom) ->
    You can also deconstruct repeated templated sections of a page into subarrays
    scraped as per your specs, by picking a context node for a dom spec. This is
    done by passing a two-element array: a selector resolving what node/nodes you
    look at and a dom spec describing how you want it/them deconstructed for you:
    on dom:
    meta:
    [ 'xpath /head',
    base: 'xpath? base
    title: 'xpath string(title)'
    ]
    commits:
    [ 'css* li.commit',
    avatar_url: ['css img.gravatar', 'xpath string(@src)']
    author_name: 'xpath string(.//*[@class="author-name"])'
    ]
    ready: (dom) ->
    The mandatory/optional selector rules defined above behave as you'd expect as
    used for context selectors too: a mandatory node or array of nodes will limit
    what pages your script gets called on to those that match it, so your code is
    free to assume it will always be there when it runs. An optional context node
    that is not found will instead result in that part of your DOM being null, or
    an empty array, in the case of a * selector.
    After you have called on(), you may call on.dom to do page scraping later on,
    returning whatever matched your selector(s) passed. Mandatory selectors which
    failed to match at this point will return undefined, optional selectors null:
    on.dom('xpath //a[@id]') => undefined or <a id="...">
    on.dom('xpath? //a[@id]') => null or <a id="...">
    on.dom('xpath+ //a[@id]') => undefined or [<a id="...">, <a id="...">, ...]
    on.dom('xpath* //a[@id]') => [] or [<a id="...">, <a id="...">, ...]
    A readable way to detect a failed mandatory match is on.dom(...) === on.FAIL;
    */

    function on(opts) {
    var Object_toString = Object.prototype.toString
    , Array_slice = Array.prototype.slice
    , FAIL = 'dom' in on ? undefined : (function() {
    var tests =
    { path_re: { fn: test_regexp, self: location.pathname }
    , query: { fn: test_query, self: location.search }
    , dom: { fn: test_dom, self: document
    , my: { 'css': not_null($C)
    , 'css?': $C
    , 'css+': one_or_more($c)
    , 'css*': $c
    , 'xpath': not_null($X)
    , 'xpath?': $X
    , 'xpath+': one_or_more($x)
    , 'xpath*': $x
    , 'xpath!': truthy($x)
    }
    }
    }
    , name, test, me, my, mine
    ;

    for (name in tests) {
    test = tests[name];
    me = test.fn;
    if ((my = test.my))
    for (mine in my)
    me[mine] = my[mine];
    on[name] = me.bind(test.self);
    }
    })()

    , input = [] // args for the callback(s?) the script wants to run
    , rules = Object.create(opts) // wraps opts in a pokeable inherit layer
    , debug = get('debug')
    , script = get('name')
    , ready = get('ready')
    , load = get('load')
    , name, rule, test, result
    ;

    if (typeof ready !== 'function' && typeof load !== 'function')
    throw new Error('on() needs at least a "ready" or "load" function!');

    try {
    for (name in rules) {
    rule = rules[name];
    if (rule === undefined) continue; // was some callback or other non-rule
    test = on[name];
    if (!test) throw new Error('did not grok rule "'+ name +'"!');
    result = test(rule);
    if (result === FAIL) return false; // the page doesn't satisfy all rules
    input.push(result);
    }
    }
    catch(e) {
    if (debug) console.warn("on(debug): we didn't run because " + e.message);
    return false;
    }

    if (ready) ready.apply(opts, input.concat());
    if (load) window.addEventListener('load', function() {
    ready.apply(opts, input.concat());
    });
    return input.concat(opts);

    function get(x) { rules[x] = undefined; return opts[x]; }
    function isArray(x) { return Object_toString.call(x) === '[object Array]'; }
    function isObject(x) { return Object_toString.call(x) === '[object Object]'; }
    function array(a) { return Array_slice.call(a, 0); } // array:ish => Array
    function arrayify(x) { return isArray(x) ? x : [x]; } // non-array? => Array

    function test_query(spec) {
    var q = unparam(this);
    if (spec === true || spec == null) return q; // decode the query for me!
    throw new Error('bad query type '+ (typeof spec) +': '+ spec);
    }

    function unparam(query) {
    var data = {};
    (query || '').replace(/\+/g, '%20').split('&').forEach(function(kv) {
    kv = /^\??([^=]*)=(.*)/.exec(kv);
    if (!kv) return;
    var prop, val, k = kv[1], v = kv[2], e, m;
    try { prop = decodeURIComponent(k); } catch (e) { prop = unescape(k); }
    try { val = decodeURIComponent(v); } catch (e) { val = unescape(v); }
    data[prop] = val;
    });
    return data;
    }

    function test_regexp(spec) {
    if (!isArray(spec)) spec = arrayify(spec);
    var re = spec.shift();
    if (typeof re === 'string') re = new RegExp(re);
    if (!(re instanceof RegExp))
    throw new Error((typeof re) +' was not a regexp: '+ re);

    var ok = re.exec(this);
    if (ok === null) return FAIL;
    if (!spec.length) return ok;
    var named = {};
    ok.shift(); // drop matching-whole-regexp part
    while (spec.length) named[spec.shift()] = ok.shift();
    return named;
    }

    function truthy(fn) { return function(s) {
    var x = fn.apply(this, arguments); return x || FAIL;
    }; }

    function not_null(fn) { return function(s) {
    var x = fn.apply(this, arguments); return x !== null ? x : FAIL;
    }; }

    function one_or_more(fn) { return function(s) {
    var x = fn.apply(this, arguments); return x.length ? x : FAIL;
    }; }

    function $c(css) { return array(this.querySelectorAll(css)); }
    function $C(css) { return this.querySelector(css); }

    function $x(xpath) {
    var doc = this.evaluate ? this : this.ownerDocument, next;
    var got = doc.evaluate(xpath, this, null, 0, null), all = [];
    switch (got.resultType) {
    case got.STRING_TYPE: return got.stringValue;
    case got.NUMBER_TYPE: return got.numberValue;
    case got.BOOLEAN_TYPE: return got.booleanValue;
    default: while ((next = got.iterateNext())) all.push(next); return all;
    }
    }
    function $X(xpath) {
    var got = $x.call(this, xpath);
    return got instanceof Array ? got[0] || null : got;
    }

    // DOM constraint tester / scraper facility:
    // "this" is the context Node(s) - initially the document
    // "spec" is either of:
    // * css / xpath Selector "selector_type selector"
    // * resolved for context [ context Selector, spec ]
    // * an Object of spec(s) { property_name: spec, ... }
    function test_dom(spec, context) {
    function lookup(rule) {
    if (typeof rule !== 'string')
    throw new Error('non-String dom match rule: '+ rule);
    var match = /^((?:css|xpath)[?+*!]?)\s+(.*)/.exec(rule), type, func;
    if (match) {
    type = match[1];
    rule = match[2];
    func = test_dom[type];
    }
    if (!func) throw new Error('unknown dom match rule '+ type +': '+ rule);
    return func.call(this, rule);
    }

    var results, result, i, property_name;
    if (context === undefined) context = this;

    // validate context:
    if (context === null || context === FAIL) return FAIL;
    if (isArray(context)) {
    for (results = [], i = 0; i < context.length; i++) {
    result = test_dom.call(context[i], spec);
    if (result === FAIL) return FAIL;
    results.push(result);
    }
    return results;
    }
    if (typeof context !== 'object' || !('nodeType' in context))
    throw new Error('illegal context: '+ context);

    // handle input spec format:
    if (typeof spec === 'string') return lookup.call(context, spec);
    if (isArray(spec)) {
    context = lookup.call(context, spec[0]);
    return test_dom.call(context, spec[1]);
    }
    if (isObject(spec)) {
    results = {};
    for (property_name in spec) {
    result = test_dom.call(context, spec[property_name]);
    if (result === FAIL) return FAIL;
    results[property_name] = result;
    }
    return results;
    }

    throw new Error("dom spec was neither a String, Object nor Array: "+ spec);
    }
    }
    6 changes: 6 additions & 0 deletions on.min.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    function on(h){function l(a){v[a]=void 0;return h[a]}function m(a){return"[object Array]"===z.call(a)}function F(a){var b={};(this||"").replace(/\+/g,"%20").split("&").forEach(function(a){if(a=/^\??([^=]*)=(.*)/.exec(a)){var c,f,g=a[1],a=a[2];try{c=decodeURIComponent(g)}catch(N){c=unescape(g)}try{f=decodeURIComponent(a)}catch(d){f=unescape(a)}b[c]=f}});if(!0===a||null==a)return b;throw Error("bad query type "+typeof a+": "+a);}function G(a){m(a)||(a=m(a)?a:[a]);var b=a.shift();"string"===typeof b&&
    (b=RegExp(b));if(!(b instanceof RegExp))throw Error(typeof b+" was not a regexp: "+b);b=b.exec(this);if(null===b)return c;if(!a.length)return b;var e={};for(b.shift();a.length;)e[a.shift()]=b.shift();return e}function A(a){return function(b){var e=a.apply(this,arguments);return null!==e?e:c}}function B(a){return function(b){var e=a.apply(this,arguments);return e.length?e:c}}function C(a){a=this.querySelectorAll(a);return H.call(a,0)}function D(a){return this.querySelector(a)}function n(a){var b=(this.evaluate?
    this:this.ownerDocument).evaluate(a,this,null,0,null),c=[];switch(b.resultType){case b.STRING_TYPE:return b.stringValue;case b.NUMBER_TYPE:return b.numberValue;case b.BOOLEAN_TYPE:return b.booleanValue;default:for(;a=b.iterateNext();)c.push(a);return c}}function E(a){a=n.call(this,a);return a instanceof Array?a[0]||null:a}function i(a,b){function e(a){if("string"!==typeof a)throw Error("non-String dom match rule: "+a);var b=/^((?:css|xpath)[?+*!]?)\s+(.*)/.exec(a),c,d;b&&(c=b[1],a=b[2],d=i[c]);if(!d)throw Error("unknown dom match rule "+
    c+": "+a);return d.call(this,a)}var d,f,g;void 0===b&&(b=this);if(null===b||b===c)return c;if(m(b)){d=[];for(g=0;g<b.length;g++){f=i.call(b[g],a);if(f===c)return c;d.push(f)}return d}if("object"!==typeof b||!("nodeType"in b))throw Error("illegal context: "+b);if("string"===typeof a)return e.call(b,a);if(m(a))return b=e.call(b,a[0]),i.call(b,a[1]);if("[object Object]"===z.call(a)){d={};for(g in a){f=i.call(b,a[g]);if(f===c)return c;d[g]=f}return d}throw Error("dom spec was neither a String, Object nor Array: "+
    a);}var z=Object.prototype.toString,H=Array.prototype.slice;if(!("dom"in on)){var p={fn:G,self:location.pathname},j={fn:F,self:location.search},q=i,r=A(D),I=B(C),J=A(E),K=B(n),L=n,p={path_re:p,query:j,dom:{fn:q,self:document,my:{css:r,"css?":D,"css+":I,"css*":C,xpath:J,"xpath?":E,"xpath+":K,"xpath*":n,"xpath!":function(a){return L.apply(this,arguments)||c}}}},k,d;for(k in p){j=p[k];q=j.fn;if(r=j.my)for(d in r)q[d]=r[d];on[k]=q.bind(j.self)}}var c=void 0,s=[],v=Object.create(h);k=l("debug");l("name");
    var t=l("ready");d=l("load");var u,w,x,y;if("function"!==typeof t&&"function"!==typeof d)throw Error('on() needs at least a "ready" or "load" function!');try{for(u in v)if(w=v[u],void 0!==w){x=on[u];if(!x)throw Error('did not grok rule "'+u+'"!');y=x(w);if(y===c)return!1;s.push(y)}}catch(M){return k&&console.warn("on(debug): we didn't run because "+M.message),!1}t&&t.apply(h,s.concat());d&&window.addEventListener("load",function(){t.apply(h,s.concat())});return s.concat(h)};