var files = { './given.js': [ '/* coffee-script example usage - at https://github.com/johan/dotjs/commits/johan' , '' , ' given 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' , ' , branches: [ Node' , ' , Node' , ' , Node' , ' , Node' , ' , Node' , ' , Node' , ' ]' , ' , dates: [ Node

Oct 07, 2012

' , ' , Node

Aug 29, 2012

' , ' , ...' , ' ]' , ' , tracker: null' , ' , johan_ci: [ Node
  • , ... ]' , ' }' , ' )' , '' , ' 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 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:' , '' , ' given 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:' , '' , ' given 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.' , '' , ' 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.' , '' , ' Once you called given(), you may call given.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:' , '' , ' given.dom(\'xpath //a[@id]\') => undefined or ' , ' given.dom(\'xpath? //a[@id]\') => null or ' , ' given.dom(\'xpath+ //a[@id]\') => undefined or [, , ...]' , ' given.dom(\'xpath* //a[@id]\') => [] or [, , ...]' , '' , ' To detect a failed mandatory match, you can use given.dom(...) === given.FAIL' , '' , ' Github pjax hook: to re-run the script\'s given() 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 given(opts, plugins) {' , ' var Object_toString = Object.prototype.toString' , ' , Array_slice = Array.prototype.slice' , ' , FAIL = \'dom\' in given ? undefined : (function() {' , ' var tests =' , ' { path_re: { fn: test_regexp }' , ' , query: { fn: test_query }' , ' , dom: { fn: test_dom' , ' , my: { \'css*\': $c' , ' , \'css+\': one_or_more($c)' , ' , \'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 }' , ' }' , ' , 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];' , ' given[name] = me;' , ' }' , ' })()' , '' , ' , 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\')' , ' , pushState = get(\'pushstate\')' , ' , pjax_event = get(\'pjaxevent\')' , ' , name, rule, test, result, retry, plugin' , ' ;' , '' , ' if (typeof ready !== \'function\' &&' , ' typeof load !== \'function\' &&' , ' typeof pushState !== \'function\') {' , ' alert(\'no given function\');' , ' throw new Error(\'given() needs at least a "ready" or "load" function!\');' , ' }' , '' , ' if (plugins)' , ' for (name in plugins)' , ' if ((rule = plugins[name]) && (test = given[name]))' , ' for (plugin in rule)' , ' if (!(test[plugin])) {' , ' given._parse_dom_rule = null;' , ' test[plugin] = rule[plugin];' , ' }' , '' , ' if (pushState && history.pushState &&' , ' (given.pushState = given.pushState || []).indexOf(opts) === -1) {' , ' given.pushState.push(opts); // make sure we don\'t reregister post-navigation' , ' initPushState(pushState, pjax_event);' , ' }' , '' , ' try {' , ' for (name in rules) {' , ' rule = rules[name];' , ' if (rule === undefined) continue; // was some callback or other non-rule' , ' test = given[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("given(debug): we didn\'t run because " + e.message);' , ' return false;' , ' }' , '' , ' if (ready) {' , ' ready.apply(opts, input.concat());' , ' }' , ' if (load) window.addEventListener(\'load\', function() {' , ' load.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 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 given_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;' , ' given(rules);' , ' };' , '' , ' document.addEventListener(\'history.pushState\', function() {' , ' if (debug) console.log(\'given.pushstate\', location.pathname);' , ' retry();' , ' }, false);' , ' }' , '' , ' function test_query(spec) {' , ' var q = unparam(this === given || 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);' , ' }' , '' , ' 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); }' , ' if ((val = v) != null)' , ' 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===given || this===window ? location.pathname : 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 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;' , ' }' , ' }' , ' function $X(xpath) {' , ' var got = $x.call(this, xpath);' , ' 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) {' , ' 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 (!given._parse_dom_rule) given._parse_dom_rule = new RegExp(\'^(\' +' , ' Object.keys(given.dom).map(quoteRe).join(\'|\') + \')\\\\s*(.*)\');' , ' var match = given._parse_dom_rule.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 === given || this === window ? document : 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)' , ' 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]);' , ' if (context === null || context === FAIL) return context;' , ' 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);' , ' }' , '};' , '' , 'if (\'module\' in this) module.exports = given;' ]};