├── .gitignore ├── src ├── config.js ├── mod │ ├── domconsole.js │ ├── events.js │ ├── utils.js │ ├── shortcuts.js │ ├── tabbing.js │ ├── sdm.js │ └── localforage.min.js ├── index.js ├── css │ ├── layout.css │ ├── content.css │ └── fonts.css ├── spec.js └── domhandler.js ├── package.json ├── index.html ├── readme.md └── config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | jspm_packages/ 3 | dist/ 4 | spec_cache.html 5 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export let config = { 2 | // SPEC_URL: '/spec_cache.html', 3 | SPEC_URL: 'http://crossorigin.me/http://www.ecma-international.org/ecma-262/6.0/index.html', 4 | IDBstoreName: 'sec' 5 | }; -------------------------------------------------------------------------------- /src/mod/domconsole.js: -------------------------------------------------------------------------------- 1 | let c = $.id('console'); 2 | export let domconsole = { 3 | log (...args) { 4 | c.classList.remove('hidden'); 5 | c.style.color = ''; 6 | c.textContent = args.join(' '); 7 | }, 8 | error (...args) { 9 | c.classList.remove('hidden'); 10 | c.style.color = 'red'; 11 | c.textContent = args.join(' '); 12 | }, 13 | clear () { 14 | c.style.color = ''; 15 | c.textContent = ''; 16 | } 17 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as Shortcuts from './mod/shortcuts'; 2 | import './mod/sdm'; 3 | import './domhandler'; 4 | import * as Spec from './spec'; 5 | import {domconsole} from './mod/domconsole'; 6 | 7 | let topbar = $.id('top-bar'), search = $.nam('search'); 8 | 9 | Shortcuts.register({modifier: 'Ctrl', key: 'P'}, function (e) { 10 | e.preventDefault(); 11 | topbar.classList.toggle('hidden'); 12 | if (topbar.classList.contains('hidden')) return; 13 | search.focus(); 14 | }); 15 | 16 | Shortcuts.register({key: 'Esc'}, function (e) { 17 | e.preventDefault(); 18 | topbar.classList.add('hidden'); 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "especser", 3 | "version": "1.0.0", 4 | "description": "browse the es spec", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Awal Garg aka Rash ", 10 | "license": "WTFPL", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/awalGarg/especser/" 14 | }, 15 | "jspm": { 16 | "directories": {}, 17 | "dependencies": { 18 | "fetch": "github:github/fetch@^0.9.0", 19 | "less": "github:aaike/jspm-less-plugin@^0.0.5", 20 | "localforage": "npm:localforage@^1.2.3" 21 | }, 22 | "devDependencies": { 23 | "babel": "npm:babel-core@^5.1.13", 24 | "babel-runtime": "npm:babel-runtime@^5.1.13", 25 | "core-js": "npm:core-js@^0.9.4" 26 | } 27 | }, 28 | "devDependencies": { 29 | "jspm": "^0.15.7", 30 | "less": "^2.5.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/mod/events.js: -------------------------------------------------------------------------------- 1 | export class EventEmitter { 2 | 3 | constructor () { 4 | this.events = { 5 | any: [] 6 | }; 7 | } 8 | 9 | on (event, listener) { 10 | if (!this.events[event]) { 11 | this.events[event] = []; 12 | } 13 | this.events[event].push(listener); 14 | } 15 | 16 | once (event, listener) { 17 | let that = this; 18 | this.on(event, function oneTimeListener (...data) { 19 | listener.apply(null, data); 20 | that.off(event, oneTimeListener); 21 | }); 22 | } 23 | 24 | off (event, listener) { 25 | if (!this.events[event]) { 26 | return; 27 | } 28 | if (!listener) { 29 | this.events[event] = []; 30 | return; 31 | } 32 | let s = this.events[event]; 33 | let i = s.indexOf(listener); 34 | if (i === -1) { 35 | return; 36 | } 37 | s.splice(i, 1); 38 | } 39 | 40 | emit (event, ...data) { 41 | let s = this.events[event]; 42 | if (!s || !s.length) return; 43 | return Promise.all( 44 | s.map( 45 | fn => Promise.resolve( 46 | fn.apply(null, data) 47 | ) 48 | ) 49 | ); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/mod/utils.js: -------------------------------------------------------------------------------- 1 | export function frag (html) { 2 | let doc = new DOMParser().parseFromString(html, 'text/html'); 3 | let frag = doc.createDocumentFragment(); 4 | let body = doc.body; 5 | while (body.firstChild) { 6 | frag.appendChild(body.firstChild); 7 | } 8 | return frag; 9 | } 10 | 11 | export function throttle (fn, time, thisArg, repeatAction) { 12 | 13 | let lastRun = false; 14 | let nextTime = 0; 15 | 16 | return function throttled (...args) { 17 | let now = Date.now(); 18 | return new Promise(function (resolve) { 19 | if (!lastRun) { 20 | lastRun = now; 21 | return resolve(fn.apply(thisArg, args)); 22 | } 23 | if ((now - lastRun) <= time) { 24 | if (repeatAction === 'discard-repeats') return; 25 | nextTime += time - (now - lastRun); 26 | return setTimeout(function () { 27 | lastRun = Date.now(); 28 | return resolve(fn.apply(thisArg, args)); 29 | }, nextTime); 30 | } 31 | lastRun = now; 32 | return resolve(fn.apply(thisArg, args)); 33 | }); 34 | }; 35 | 36 | } 37 | 38 | export let re = { 39 | escape: function RegexpEscape (s) { 40 | return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); 41 | } 42 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | especser 6 | 7 | 8 | 9 | 10 |
11 | especser 12 |
13 | 14 |
    15 |
    16 |
    17 | 18 | 19 | 20 |
    21 |
    22 |
    23 | 27 |
    28 |
    especser is an in-browser app to browse and read the ECMA-262 ECMAScript Standard Specification Edition 6.0.
    Written by Awal Garg aka Rash. on github at https://github.com/awalGarg/especser/
    29 |
    30 |
    31 |
    
    32 | 
    33 | 
    36 | 
    37 | 
    
    
    --------------------------------------------------------------------------------
    /src/mod/shortcuts.js:
    --------------------------------------------------------------------------------
     1 | let shortcuts = {
     2 | 	keypress: {},
     3 | 	keydown: {}
     4 | };
     5 | 
     6 | let Key_Mappings = {
     7 | 	F: 70,
     8 | 	P: 80,
     9 | 	ESC: 27
    10 | };
    11 | 
    12 | document.addEventListener('keypress', handler, true);
    13 | document.addEventListener('keydown', handler, true);
    14 | 
    15 | function handler (ev) {
    16 | 	let mods = [];
    17 | 	if (ev.altKey) mods.push('alt');
    18 | 	if (ev.ctrlKey) mods.push('ctrl');
    19 | 	if (ev.shiftKey) mods.push('shift');
    20 | 	mods = mods.sort();
    21 | 	let keyCode = ev.keyCode;
    22 | 	let id = `${mods.join('+')}:${keyCode}`;
    23 | 
    24 | 	if (shortcuts[ev.type].hasOwnProperty(id)) {
    25 | 		let {callback, thisArg, args} = shortcuts[ev.type][id];
    26 | 		callback.call(thisArg, ev, ...args);
    27 | 	}
    28 | }
    29 | 
    30 | function findKeyCode (key) {
    31 | 	return Key_Mappings[key.toUpperCase()];
    32 | }
    33 | 
    34 | function parseKey ({modifier = [], key = ''}) {
    35 | 	if (!Array.isArray(modifier)) modifier = [modifier];
    36 | 	let mods = modifier.map(m => m.toLowerCase()).sort();
    37 | 	let evType, keyCode;
    38 | 	if (!modifier.length && String(key).length === 1) {
    39 | 		evType = 'keypress';
    40 | 		keyCode = key.toLowerCase().charCodeAt();
    41 | 	}
    42 | 	else {
    43 | 		evType = 'keydown';
    44 | 		keyCode = key;
    45 | 		if (!Number(key)) {
    46 | 			keyCode = findKeyCode(key);
    47 | 		}
    48 | 	}
    49 | 	if (!keyCode) throw new Error(`Unable to parse key definition. Passed modifier: ${modifier}. Passed Key: ${key}.`);
    50 | 	let id = `${mods.join('+')}:${keyCode}`;
    51 | 	return { mods, id, keyCode, evType, key };
    52 | }
    53 | 
    54 | export function register (definition, callback, thisArg, ...args) {
    55 | 	let descriptor = parseKey(definition);
    56 | 	let data = {callback, thisArg, args};
    57 | 	shortcuts[descriptor.evType][descriptor.id] = data;
    58 | }
    59 | 
    60 | export function remove (definition) {
    61 | 	let {evType, id} = parseKey(definition);
    62 | 	delete shortcuts[evType][id];
    63 | }
    64 | 
    65 | export function removeAll () {
    66 | 	shortcuts = {
    67 | 		keypress: {},
    68 | 		keydown: {}
    69 | 	};
    70 | }
    
    
    --------------------------------------------------------------------------------
    /readme.md:
    --------------------------------------------------------------------------------
     1 | #especser
     2 | 
     3 | especser is an in browser app to browse and read the ECMA-262 standard's 6th edition for ECMAScript standardized by ECMAInternational
     4 | 
     5 | ##why
     6 | 
     7 | the es specs are pretty much awful to read. even more awful to navigate. especser presents the spec in an easy on eyes way, with fuzzy searching of the different sections in the spec, tabbed browsing support, internal routing of links etc.
     8 | 
     9 | ##how
    10 | 
    11 | 1. goto http://awalGarg.github.io/especser
    12 | 2. click the update button on the corner
    13 | 	3. this will fetch the spec directly from http://ecma-international.org/ecma-262/6.0/index.html, pass it through some weirdo functions, store a json map and different parts of the spec in indexedDB
    14 | 	4. you only have to do this once, and the spec will only be fetched once
    15 | 5. you can search for stuff you want. use up/down arrow keys to select one of the results and hit enter (or click on one of the search result)
    16 | 6. press `Ctrl+P` to toggle the top-bar
    17 | 7. opened tabs are shown on the sidebar on the left.
    18 | 8. top of the content is shown a path to the present page. you can click on any link in between to open that
    19 | 9. bottom of the content is shown a list of links to sub-sections for the page if any
    20 | 10. click on the large index number at the top-left of any content page and copy the url in the address bar to share a perma-link to that section with anyone else
    21 | 11. links inside the spec are internal. so clicking on any link inside the spec will open that section within the app
    22 | 12. you can remove the data from indexedDB by clicking the clear store button
    23 | 13. keep hitting the down arrow key while searching to extract more results
    24 | 14. if your search starts with `sec: `, it will list all those sections and subsections. if a query follows, only sections matching it will be listed.
    25 | 
    26 | ##running locally
    27 | 
    28 | - clone repo
    29 | - `npm install`
    30 | - `jspm install`
    31 | - `iojs build.js`
    32 | - start a webserver in the project root
    33 | - open the url to the server
    34 | 
    35 | ##screenshots
    36 | 
    37 | ofcourse
    38 | 
    39 | ![seeing a simple page in especser with some tabs open](http://i.imgur.com/lkIcVon.png)
    40 | 
    41 | ![searching for something](http://i.imgur.com/xz0eNK2.png)
    42 | 
    43 | ##license
    44 | 
    45 | WTFPL
    
    
    --------------------------------------------------------------------------------
    /src/mod/tabbing.js:
    --------------------------------------------------------------------------------
      1 | /*jslint esnext: true*/
      2 | 
      3 | import {EventEmitter} from './events';
      4 | 
      5 | let Utils = {
      6 | 	randString () {
      7 | 		return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, function () {
      8 | 			return String.fromCharCode(Math.floor(Math.random()*120)+1);
      9 | 		});
     10 | 	}
     11 | };
     12 | 
     13 | export class TabGroup {
     14 | 
     15 | 	constructor () {
     16 | 		this.events = new EventEmitter();
     17 | 		this.tabs = Object.create(null);
     18 | 		this.store = new WeakMap();
     19 | 		this.activeTabId = null;
     20 | 		this.history = [];
     21 | 	}
     22 | 
     23 | 	attach (tab) {
     24 | 		if (!(tab instanceof Tab)) {
     25 | 			throw new Error('Expected tab to be of instance Tab. Unknown type passed.');
     26 | 		}
     27 | 		if (this.store.has(tab)) {
     28 | 			throw new Error('Tab is already in this TabGroup. Aborting.');
     29 | 		}
     30 | 		let id = Utils.randString();
     31 | 		this.tabs[id] = tab;
     32 | 		this.store.set(tab, id);
     33 | 		this.status = {
     34 | 			type: 'attach',
     35 | 			id
     36 | 		};
     37 | 		return id;
     38 | 	}
     39 | 
     40 | 	detach (thing) {
     41 | 		let {tab, id} = this.find(thing);
     42 | 		if (this.activeTabId === id) {
     43 | 			this.close(thing);
     44 | 		}
     45 | 		delete this.tabs[id];
     46 | 		this.status = {
     47 | 			type: 'detach',
     48 | 			id
     49 | 		};
     50 | 		this.store.delete(tab);
     51 | 	}
     52 | 
     53 | 	open (thing) {
     54 | 		let {tab, id} = this.find(thing);
     55 | 		if (this.activeTabId === id) return;
     56 | 		if (this.activeTabId) {
     57 | 			this.close(this.activeTabId);
     58 | 		}
     59 | 		this.activeTabId = id;
     60 | 		this.status = {
     61 | 			type: 'open',
     62 | 			id
     63 | 		};
     64 | 	}
     65 | 
     66 | 	close (thing) {
     67 | 		let {tab, id} = this.find(thing);
     68 | 		if (this.activeTabId !== id) return;
     69 | 		this.activeTabId = null;
     70 | 		this.status = {
     71 | 			type: 'close',
     72 | 			id
     73 | 		};
     74 | 	}
     75 | 
     76 | 	restoreLast () {
     77 | 		let last;
     78 | 		for (let i = this.history.length - 1; i >= 0; i--) {
     79 | 			let desc = this.history[i];
     80 | 			if (desc.type === 'close' && typeof this.tabs[desc.id] !== 'undefined') {
     81 | 				last = desc.id;
     82 | 				break;
     83 | 			}
     84 | 		}
     85 | 		if (!last) {
     86 | 			return;
     87 | 		}
     88 | 		this.open(last);
     89 | 	}
     90 | 
     91 | 	find (thing) {
     92 | 		let tab, id;
     93 | 		if (typeof thing === 'string') {
     94 | 			if (typeof this.tabs[thing] === 'undefined') {
     95 | 				throw new Error('TabGroup contains no tab with id ' + thing);
     96 | 			}
     97 | 			tab = this.tabs[thing];
     98 | 			id = thing;
     99 | 		} else if (thing instanceof Tab) {
    100 | 			if (this.store.has(thing)) {
    101 | 				tab = thing;
    102 | 				id = this.store.get(thing);
    103 | 			} else {
    104 | 				throw new Error('This tab is not a part of this TabGroup.');
    105 | 			}
    106 | 		} else {
    107 | 			throw new Error('Unidentified object passed.');
    108 | 		}
    109 | 		return {tab, id};
    110 | 	}
    111 | 
    112 | 	set status (stat) {
    113 | 		let type = stat.type;
    114 | 		if (!type) throw new Error('Unable to set status. No valid type found.');
    115 | 
    116 | 		this.history.push(stat);
    117 | 		this.events.emit(type, stat);
    118 | 	}
    119 | 
    120 | 	get status () {
    121 | 		return this.history[this.history.length - 1];
    122 | 	}
    123 | 
    124 | }
    125 | 
    126 | export class Tab {
    127 | 
    128 | 	constructor (title, description, meta) {
    129 | 		this.title = title;
    130 | 		this.description = description;
    131 | 		this.meta = meta;
    132 | 	}
    133 | 
    134 | 	attachTo (group) {
    135 | 		if (!(group instanceof TabGroup)) {
    136 | 			throw new Error('Expected group to be instance of TabGroup. Unidentifiable object passed.');
    137 | 		}
    138 | 		group.attach(this);
    139 | 	}
    140 | 
    141 | }
    
    
    --------------------------------------------------------------------------------
    /config.js:
    --------------------------------------------------------------------------------
      1 | System.config({
      2 |   "baseURL": "/",
      3 |   "transpiler": "babel",
      4 |   "babelOptions": {
      5 |     "optional": [
      6 |       "runtime"
      7 |     ],
      8 |     "blacklist": [
      9 |       "asyncToGenerator",
     10 |       "bluebirdCoroutines",
     11 |       "es3.memberExpressionLiterals",
     12 |       "es3.propertyLiterals",
     13 |       "es5.properties.mutators",
     14 |       // "es6.arrowFunctions",
     15 |       // "es6.blockScoping",
     16 |       // "es6.classes",
     17 |       // "es6.constants",
     18 |       // "es6.destructuring",
     19 |       // "es6.forOf",
     20 |       // "es6.modules",
     21 |       // "es6.objectSuper",
     22 |       // "es6.parameters.default",
     23 |       // "es6.parameters.rest",
     24 |       // "es6.properties.computed",
     25 |       // "es6.properties.shorthand",
     26 |       "es6.regex.sticky",
     27 |       "es6.regex.unicode",
     28 |       // "es6.spec.blockScoping",
     29 |       "es6.spec.symbols",
     30 |       // "es6.spec.templateLiterals",
     31 |       // "es6.spread",
     32 |       // "es6.tailCall",
     33 |       // "es6.templateLiterals",
     34 |       "es7.asyncFunctions",
     35 |       "es7.classProperties",
     36 |       "es7.comprehensions",
     37 |       "es7.decorators",
     38 |       "es7.doExpressions",
     39 |       "es7.exponentiationOperator",
     40 |       "es7.exportExtensions",
     41 |       "es7.objectRestSpread",
     42 |       "es7.trailingFunctionCommas",
     43 |       "flow",
     44 |       "jscript",
     45 |       // "ludicrous",
     46 |       "minification.deadCodeElimination",
     47 |       // "minification.memberExpressionLiterals",
     48 |       // "minification.propertyLiterals",
     49 |       "optimisation.flow.forOf",
     50 |       "optimisation.react.constantElements",
     51 |       "optimisation.react.inlineElements",
     52 |       "react",
     53 |       "reactCompat",
     54 |       // "regenerator",
     55 |       // "runtime",
     56 |       "spec.blockScopedFunctions",
     57 |       "spec.functionName",
     58 |       "spec.protoToAssign",
     59 |       "spec.undefinedToVoid",
     60 |       "strict",
     61 |       "utility.inlineEnvironmentVariables",
     62 |       "minification.constantFolding",
     63 |       // "utility.removeConsole",
     64 |       "minification.removeDebugger",
     65 |       "validation.react",
     66 |       "validation.undeclaredVariableCheck"
     67 |     ]
     68 |   },
     69 |   "paths": {
     70 |     "*": "*.js",
     71 |     "github:*": "jspm_packages/github/*.js",
     72 |     "npm:*": "jspm_packages/npm/*.js"
     73 |   }
     74 | });
     75 | 
     76 | System.config({
     77 |   "map": {
     78 |     "babel": "npm:babel-core@5.5.8",
     79 |     "babel-runtime": "npm:babel-runtime@5.5.8",
     80 |     "core-js": "npm:core-js@0.9.18",
     81 |     "fetch": "github:github/fetch@0.9.0",
     82 |     "less": "github:aaike/jspm-less-plugin@0.0.5",
     83 |     "localforage": "npm:localforage@1.2.3",
     84 |     "github:aaike/jspm-less-plugin@0.0.5": {
     85 |       "less.js": "github:distros/less@2.4.0"
     86 |     },
     87 |     "github:jspm/nodelibs-path@0.1.0": {
     88 |       "path-browserify": "npm:path-browserify@0.0.0"
     89 |     },
     90 |     "github:jspm/nodelibs-process@0.1.1": {
     91 |       "process": "npm:process@0.10.1"
     92 |     },
     93 |     "npm:asap@1.0.0": {
     94 |       "process": "github:jspm/nodelibs-process@0.1.1"
     95 |     },
     96 |     "npm:babel-runtime@5.5.8": {
     97 |       "process": "github:jspm/nodelibs-process@0.1.1"
     98 |     },
     99 |     "npm:core-js@0.9.18": {
    100 |       "fs": "github:jspm/nodelibs-fs@0.1.2",
    101 |       "process": "github:jspm/nodelibs-process@0.1.1",
    102 |       "systemjs-json": "github:systemjs/plugin-json@0.1.0"
    103 |     },
    104 |     "npm:localforage@1.2.3": {
    105 |       "path": "github:jspm/nodelibs-path@0.1.0",
    106 |       "process": "github:jspm/nodelibs-process@0.1.1",
    107 |       "promise": "npm:promise@5.0.0"
    108 |     },
    109 |     "npm:path-browserify@0.0.0": {
    110 |       "process": "github:jspm/nodelibs-process@0.1.1"
    111 |     },
    112 |     "npm:promise@5.0.0": {
    113 |       "asap": "npm:asap@1.0.0"
    114 |     }
    115 |   }
    116 | });
    117 | 
    118 | 
    
    
    --------------------------------------------------------------------------------
    /src/mod/sdm.js:
    --------------------------------------------------------------------------------
      1 | ;(function (window, document) {
      2 |     
      3 |     /**
      4 |      * simple dom module (sdm)
      5 |      * Copyright (c) Awal Garg aka Rash 
      6 |      * License WTFPL
      7 |      */
      8 |     
      9 |     "use strict";
     10 |     
     11 |     function slice (stuff) {
     12 |         return stuff && Array.prototype.slice.call(stuff);
     13 |     }
     14 |     
     15 |     function type (stuff) {
     16 |         return ({}).toString.call(stuff).replace('[object ', '').replace(']', '').toLowerCase();
     17 |     }
     18 | 
     19 |     var $ = window.$ = function (sel, parent) {
     20 |         return (parent||document).querySelector(sel);
     21 |     };
     22 |     var $$ = window.$$ = function (sel, parent) {
     23 |         return slice((parent||document).querySelectorAll(sel));
     24 |     };
     25 |     function wrapQuotes (val) {
     26 |         if (val) return '"' + val + '"';
     27 |         return val;
     28 |     }
     29 |     $.id = function (id, parent) {
     30 |         return (parent || document).getElementById(id);
     31 |     };
     32 |     $.cl = function (cl, parent) {
     33 |         return $$.cl(cl, parent)[0];
     34 |     };
     35 |     $.nam = function (nam, parent) {
     36 |         return $$.nam(nam, parent)[0];
     37 |     };
     38 |     $.tag = function (tag, parent) {
     39 |         return $$.tag(tag, parent)[0];
     40 |     };
     41 |     $.attr = function (attr, val, parent) {
     42 |         if (typeof val === 'undefined') val = '';
     43 |         return (parent||document).querySelector('[' + attr + wrapQuotes(val) + ']');
     44 |     };
     45 |     $.data = function (set, val, parent) {
     46 |         return $.attr('data-' + set, val, parent);
     47 |     };
     48 |     
     49 |     $$.cl = function (cl, parent) {
     50 |         return slice((parent||document).getElementsByClassName(cl));
     51 |     };
     52 |     $$.nam = function (nam, parent) {
     53 |         return slice((parent||document).getElementsByName(nam));
     54 |     };
     55 |     $$.tag = function (tag, parent) {
     56 |         return slice((parent||document).getElementsByTagName(tag));
     57 |     };
     58 |     $$.attr = function (attr, val, parent) {
     59 |         if (typeof val === 'undefined') val = '';
     60 |         return slice((parent||document).querySelectorAll('[' + attr + wrapQuotes(val) + ']'));
     61 |     };
     62 |     $$.data = function (set, val, parent) {
     63 |         return $$.attr('data-' + set, val, parent);
     64 |     };
     65 |     
     66 |     function assignProps (obj, stuff) {
     67 |         if (obj && stuff) Object.keys(stuff).forEach(function (key) {
     68 |             obj[key] = stuff[key];
     69 |         });
     70 |     }
     71 |     
     72 |     $.apply = function (el, opts) {
     73 | 
     74 |         if (!opts) return el;
     75 | 
     76 |         assignProps(el.style, opts.style);
     77 |         delete opts.style;
     78 |         assignProps(el.dataset, opts.dataset);
     79 |         delete opts.dataset;
     80 |         if (opts.classList) opts.classList.forEach(function (cl) {
     81 |             el.classList.add(cl);
     82 |         });
     83 |         delete opts.dataset;
     84 |         if (opts.childNodes) opts.childNodes.forEach(function (child) {
     85 |             el.appendChild(child);
     86 |         });
     87 |         delete opts.childNodes;
     88 |         var events = opts.on;
     89 |         if (events) Object.keys(events).forEach(function (ev) {
     90 |             var det = events[ev];
     91 |             if (type(det) !== 'array') det = [det];
     92 |             det.forEach(function(li) {
     93 |                 var maybeCapture = type(li) === 'array';
     94 |                 el.addEventListener(
     95 |                     ev,
     96 |                     maybeCapture ? li[0] : li,
     97 |                     maybeCapture ? li[1] : false
     98 |                 );
     99 |             });
    100 |         });
    101 |         delete opts.on;
    102 |         if (opts.attributes) Object.keys(opts.attributes).forEach(function (attr) {
    103 |             el.setAttribute(attr, opts.attributes[attr]);
    104 |         });
    105 | 
    106 |         
    107 |         Object.keys(opts).forEach(function (key) {
    108 |             try {
    109 |                 el[key] = opts[key];
    110 |             }
    111 |             catch (e) {}
    112 |         });
    113 |         
    114 |         return el;
    115 | 
    116 |     };
    117 |     
    118 |     $.make = function make (sign, opts) {
    119 |         
    120 |         if (sign === '#text') return document.createTextNode(opts);
    121 | 
    122 |         if (sign === '#frag') return $.apply(
    123 |             document.createDocumentFragment(),
    124 |             {childNodes: opts && opts.childNodes}
    125 |         );
    126 |         
    127 |         var el;
    128 |         
    129 |         if (typeof sign === 'string') {
    130 |             el = document.createElement(sign);
    131 |         }
    132 |         else {
    133 |             el = sign.cloneNode(opts && opts.deep);
    134 |         }
    135 | 
    136 |         if (!opts) return el;
    137 | 
    138 |         delete opts.deep;
    139 |         
    140 |         return $.apply(el, opts);
    141 |     };
    142 | 
    143 |     $.append = function (elem, refElem, position) {
    144 |         position = (position || "bottom").toLowerCase();
    145 | 
    146 |         if (position === "top") {
    147 |             if (!refElem.childNodes.length) return refElem.appendChild(elem);
    148 |             return refElem.insertBefore(elem, refElem.firstChild);
    149 |         }
    150 |         else if (position === "bottom") {
    151 |             return refElem.appendChild(elem);
    152 |         }
    153 |         else if (position === "before") {
    154 |             return refElem.parentNode.insertBefore(elem, refElem);
    155 |         }
    156 |         else if (position === "after") {
    157 |             if (!refElem.nextElementSibling) return refElem.parentNode.appendChild(elem);
    158 |             return refElem.parentNode.insertBefore(elem, refElem.nextElementSibling);
    159 |         }
    160 |         else if (position === "replace") {
    161 |             return refElem.parentNode.replaceChild(elem, refElem);
    162 |         }
    163 |         else {
    164 |             throw new Error('Unknown position specified. Expected "top", "bottom", "before", "after" or "replace".');
    165 |         }
    166 | 
    167 |     };
    168 | 
    169 |     $.remove = function (node) {
    170 |         if (typeof node === 'string') node = $(node);
    171 |         if (node && node.parentNode) node.parentNode.removeChild(node);
    172 |     };
    173 | 
    174 | })(window, document);
    
    
    --------------------------------------------------------------------------------
    /src/css/layout.css:
    --------------------------------------------------------------------------------
      1 | *, *:after, *:before {
      2 |     box-sizing: border-box;
      3 |     outline: 0px;
      4 |     margin: 0px;
      5 | }
      6 | input, form, fieldset, div, p, section, body, html {
      7 |     padding: 0px;
      8 | }
      9 | body {
     10 |     background-color: #303030;
     11 |     color: #c9c9c9;
     12 |     font: normal 1em/1.7 "Open Sans", Helvetica, Arial, sans-serif;
     13 |     height: 100vh;
     14 |     width: 100vw;
     15 |     word-wrap: break-word;
     16 |     overflow-wrap: break-word;
     17 |     display: flex;
     18 |     flex-direction: column;
     19 | }
     20 | 
     21 | /* layout */
     22 | #top-bar {
     23 |     background: #212121;
     24 |     width: 100%;
     25 |     display: flex;
     26 |     border: 1px solid #333;
     27 |     position: absolute;
     28 |     z-index: 5;
     29 | 
     30 |     .logoish {
     31 |         color: inherit;
     32 |         padding: 0 1rem;
     33 |         font-style: italic;
     34 |         text-shadow: 0px 0px 0.2rem;
     35 |         text-decoration: none;
     36 |     }
     37 | 
     38 |     #search-form {
     39 |         flex-grow: 1;
     40 |         height: 2rem;
     41 |         line-height: 100%;
     42 |         background-color: inherit;
     43 | 
     44 |         input {
     45 |             width: 100%;
     46 |             height: 2rem;
     47 |             border-radius: 0;
     48 |             border: 1px solid #333;
     49 |             border-width: 0px 1px 0px 0px;
     50 |             font: inherit;
     51 |             font-size:  0.875rem;
     52 |             background-color: rgba(0, 0, 0, 0.5);
     53 |             color: #fff;
     54 |             padding: 0em 1rem;
     55 |             position: relative;
     56 |         }
     57 |         #search-results {
     58 |             background-color: inherit;
     59 |             border: 1px solid #333;
     60 |             padding: 0;
     61 |             list-style: none;
     62 |             max-height: 80vh;
     63 |             overflow-y: auto;
     64 | 
     65 |             .result {
     66 |                 display: block;
     67 |                 padding: 0.5rem 1rem;
     68 |                 font-size: 0.875rem;
     69 |                 color: inherit;
     70 |                 text-decoration: none;
     71 | 
     72 |                 .result-heading {
     73 |                     font-weight: normal;
     74 |                 }
     75 |                 .result-path, .result-index {
     76 |                     font-size: small;
     77 |                     padding-right: 0.5rem;
     78 |                 }
     79 |             }
     80 |             .result.active {
     81 |                 background-color: #4C4C4C;
     82 |             }
     83 |         }
     84 |     }
     85 |     .option-box {
     86 |         white-space: nowrap;
     87 |         margin: 0 1em;
     88 | 
     89 |         button {
     90 |             background: transparent;
     91 |             color: inherit;
     92 |             border-width: 0px;
     93 |             font: inherit;
     94 |             font-size: 0.8rem;
     95 |             padding: 0 0.2rem;
     96 |             cursor: pointer;
     97 |         }
     98 |     }
     99 | }
    100 | 
    101 | .content-wrapper {
    102 |     display: flex;
    103 |     flex-grow: 1;
    104 |     position: relative;
    105 | 
    106 |     #sidebar {
    107 |         position: relative;
    108 |         min-width: 20vw;
    109 |         max-width: 20vw;
    110 |         border-right: 1px solid rgba(255, 255, 255, 0.2);
    111 |         position: relative;
    112 |         overflow-y: auto;
    113 | 
    114 |         #open-tab-descriptors {
    115 |             list-style: none;
    116 |             color: inherit;
    117 |             padding: 0;
    118 |             font-size: 0.875rem;
    119 |             white-space: nowrap;
    120 | 
    121 |             li {
    122 |                 height: 2.5rem;
    123 |                 border: 1px inset #6b6b6b;
    124 |                 display: flex;
    125 |                 align-items: center;
    126 |                 font-weight: bold;
    127 |                 padding: 0;
    128 |                 border-width: 1px 0px;
    129 | 
    130 |                 div.tab-descriptor.link-tab-descriptor {
    131 |                     display: flex;
    132 |                     text-overflow: ellipsis;
    133 |                     overflow: hidden;
    134 |                     align-items: center;
    135 |                     width: 100%;
    136 |                     padding: 0 1rem;
    137 |                     height: 100%;
    138 | 
    139 |                     a.link-tab-activate {
    140 |                         flex-grow: 1;
    141 |                         color: inherit;
    142 |                         text-decoration: inherit;
    143 |                         text-overflow: ellipsis;
    144 |                         overflow: hidden;
    145 |                     }
    146 | 
    147 |                     span.tab-close {
    148 |                         display: inline-block;
    149 |                         margin-left: auto;
    150 |                     }
    151 | 
    152 |                     span.tab-close:before {
    153 |                         content: '✕';
    154 |                     }
    155 |                 }
    156 | 
    157 |                 div.tab-descriptor.link-tab-descriptor.active {
    158 |                     background-color: #232323;
    159 |                 }
    160 |             }
    161 | 
    162 |             li:first-of-type {
    163 |                 border-top: 1px inset #111;
    164 |             }
    165 |         }
    166 | 
    167 |     }
    168 | 
    169 |     #content {
    170 |         flex-grow: 1;
    171 |         overflow-x: hidden;
    172 |         overflow-y: auto;
    173 |         position: relative;
    174 |     }
    175 | 
    176 |     .__info {
    177 |         border: 1px solid #111;
    178 |         background: #232323;
    179 |         color: white;
    180 |         padding: 0.5rem 1rem;
    181 |         border-radius: 0.3rem;
    182 |         font: normal 0.875rem 'Open Sans';
    183 | 
    184 |         a {
    185 |             text-decoration: inherit;
    186 |             color: inherit;
    187 |             font-weight: inherit;
    188 |         }
    189 | 
    190 |         .__info-path a {
    191 |             margin-right: 0.5rem;
    192 |         }
    193 | 
    194 |         .__info-path a:after {
    195 |             content: '»';
    196 |             margin-left: 0.5rem;
    197 |         }
    198 | 
    199 |         .__info-path a:last-of-type:after {
    200 |             content: '';
    201 |         }
    202 | 
    203 |         .__info-label {
    204 |             display: inline-block;
    205 |             line-height: 200%;
    206 |         }
    207 |     }
    208 | 
    209 |     .__info:first-of-type {
    210 |         margin-bottom: 2rem;
    211 |     }
    212 | 
    213 |     .__info:last-of-type {
    214 |         margin-top: 2rem;
    215 |     }
    216 | 
    217 | 
    218 |     .__info-children span {
    219 |         margin-right: 0.5rem;
    220 |     }
    221 | 
    222 | }
    223 | 
    224 | #console {
    225 |     position: absolute;
    226 |     bottom: 0;
    227 |     width: 100%;
    228 |     color: white;
    229 |     background-color: #111;
    230 | }
    231 | 
    232 | /* meta classes */
    233 | .abs {
    234 |     position: absolute;
    235 | }
    236 | .hidden {
    237 |     display: none!important;
    238 | }
    239 | .reveal-on-parent-hover {
    240 |     display: none;
    241 | }
    242 | *:hover .reveal-on-parent-hover {
    243 |     display: block;
    244 | }
    245 | .static-background {
    246 |     bottom: 10px;
    247 |     background-color: inherit;
    248 |     position: fixed;
    249 |     display: flex;
    250 |     justify-content: center;
    251 |     align-items: center;
    252 |     padding: 0 1rem;
    253 |     opacity: 0.5;
    254 |     z-index: -1;
    255 | }
    256 | 
    
    
    --------------------------------------------------------------------------------
    /src/css/content.css:
    --------------------------------------------------------------------------------
      1 | /**
      2 |  * directly copied from ecmascript's own css
      3 |  */
      4 | .tab-content { /* this one is slightly modified... maaybe */
      5 | 	background-color: #fdfdfc;
      6 |     color: #333333;
      7 |     font-family: Cambria, Palatino Linotype, Palatino, Liberation Serif, serif;
      8 |     font-size: 17px;
      9 |     line-height: 135%;
     10 |     padding: 1rem;
     11 | 
     12 | 	h1 {
     13 | 		font-size: 110%;
     14 | 		margin-bottom: 0.8em;
     15 | 		padding-bottom: 0.2em;
     16 | 		position: sticky;
     17 | 		top: 0;
     18 | 		z-index: 1;
     19 | 		background-color: #fdfdfc;
     20 | 		box-shadow: 0 0 6px #fdfdfc;
     21 | 		margin-left: -7em;
     22 | 		padding-left: 7em;
     23 | 	}
     24 | 
     25 | 	.stern.warning {
     26 | 	  font-weight: bold;
     27 | 	  text-transform: uppercase;
     28 | 	}
     29 | 
     30 | 	p.ECMAaddress {
     31 | 	  margin-bottom: 0em;
     32 | 	  margin-top: 0em;
     33 | 	}
     34 | 
     35 | 	p.normalBullet {
     36 | 	  margin-left: 4em;
     37 | 	  text-indent: -1.7em;
     38 | 	  margin-top: 0em;
     39 | 	  margin-bottom: 0.25em;
     40 | 	}
     41 | 
     42 | 	p.normalBulletSubstep {
     43 | 	  margin-left: 6em;
     44 | 	  text-indent: -1em;
     45 | 	  margin-top: 0em;
     46 | 	  margin-bottom: 0.25em;
     47 | 	}
     48 | 
     49 | 	pre.NoteCode {
     50 | 	  margin-bottom: 0em;
     51 | 	  margin-top: 0em;
     52 | 	}
     53 | 
     54 | 	div.note {
     55 | 	  margin: 1em 0 1em 6em;
     56 | 	}
     57 | 
     58 | 	span.nh {
     59 | 	  float: left;
     60 | 	  width: 6em;
     61 | 	  margin-left: -6em;
     62 | 	}
     63 | 
     64 | 	figure {
     65 | 	  display: block;
     66 | 	  margin: 1em 0 3em 0;
     67 | 	}
     68 | 
     69 | 	figure object {
     70 | 	  display: block;
     71 | 	  margin: 0 auto;
     72 | 	}
     73 | 
     74 | 	figure table.real-table {
     75 | 	  margin: 0 auto;
     76 | 	}
     77 | 
     78 | 	figure figcaption {
     79 | 	  display: block;
     80 | 	  color: #555555;
     81 | 	  font-weight: bold;
     82 | 	  text-align: center;
     83 | 	  margin-bottom: 0.25em;
     84 | 	}
     85 | 
     86 | 	figcaption :target {
     87 | 	  /* When a user visits #table-1, slide the caption down
     88 | 	         so it won't be obscured by a sticky heading (issue #73).
     89 | 	         Thanks to Claude Pache for this "nasty trick" (his words). */
     90 | 	  display: inline-block;
     91 | 	  padding-top: 2em;
     92 | 	  margin-top: -2em;
     93 | 	}
     94 | 
     95 | 	table.real-table {
     96 | 	  border-collapse: collapse;
     97 | 	}
     98 | 
     99 | 	table.real-table td, table.real-table th {
    100 | 	  border: 1px solid black;
    101 | 	  padding: 0.4em;
    102 | 	  vertical-align: baseline;
    103 | 	}
    104 | 
    105 | 	table.real-table th {
    106 | 	  background-color: #eeeeee;
    107 | 	}
    108 | 
    109 | 	table.lightweight-table {
    110 | 	  border-collapse: collapse;
    111 | 	  margin: 0 0 0 1.5em;
    112 | 	}
    113 | 
    114 | 	table.lightweight-table td, table.lightweight-table th {
    115 | 	  border: none;
    116 | 	  padding: 0 0.5em;
    117 | 	  vertical-align: baseline;
    118 | 	}
    119 | 
    120 | 	div.display {
    121 | 	  margin: 1em 0 1em 2em;
    122 | 	}
    123 | 
    124 | 	div.gp {
    125 | 	  margin-left: 2.4em;
    126 | 	  margin-top: 0.9em;
    127 | 	}
    128 | 
    129 | 	div.gp.prod {
    130 | 	  margin-left: 0;
    131 | 	}
    132 | 
    133 | 	div.rhs {
    134 | 	  margin-left: 2.4em;
    135 | 	  margin-right: -10em;
    136 | 	}
    137 | 
    138 | 	div.pile {
    139 | 	  margin-left: 2.4em;
    140 | 	  max-width: 40em;
    141 | 	}
    142 | 
    143 | 	div.keyword.pile code {
    144 | 	  float: left;
    145 | 	  width: 25%;
    146 | 	}
    147 | 
    148 | 	div.keyword5.pile code {
    149 | 	  float: left;
    150 | 	  width: 20%;
    151 | 	}
    152 | 
    153 | 	div.operator.pile code {
    154 | 	  float: left;
    155 | 	  width: 16%;
    156 | 	}
    157 | 
    158 | 	div.end-pile {
    159 | 	  clear: both;
    160 | 	}
    161 | 
    162 | 	sub.g-opt {
    163 | 	  color: #d1009e;
    164 | 	  /* sort of magenta */
    165 | 	}
    166 | 
    167 | 	sub.g-params {
    168 | 	  color: #49a08a;
    169 | 	  /* sort of aqua */
    170 | 	}
    171 | 
    172 | 	code {
    173 | 	  /* including code.t, meaning "terminal" or "token" */
    174 | 	  font-weight: bold;
    175 | 	  color: #555555;
    176 | 	}
    177 | 
    178 | 	span.nt {
    179 | 	  /* nonterminal */
    180 | 	  font-family: Times New Roman, Times, FreeSerif, serif;
    181 | 	  font-style: italic;
    182 | 	}
    183 | 
    184 | 	span.geq {
    185 | 	  font-weight: bold;
    186 | 	}
    187 | 
    188 | 	span.grhsmod {
    189 | 	  /* right-hand side modifier, like "one of", "but not" */
    190 | 	  font-weight: bold;
    191 | 	  color: #555555;
    192 | 	}
    193 | 
    194 | 	span.grhsannot {
    195 | 	  /* right-hand side annotation, like "[empty]" */
    196 | 	  font-family: Helvetica, Arial, Liberation Sans, sans-serif;
    197 | 	  font-size: smaller;
    198 | 	}
    199 | 
    200 | 	span.chgloss {
    201 | 	  /* gloss for a character, like "asterisk" */
    202 | 	  font-style: italic;
    203 | 	}
    204 | 
    205 | 	span.gprose {
    206 | 	  font-family: Helvetica, Arial, Liberation Sans, sans-serif;
    207 | 	  font-size: 90%;
    208 | 	}
    209 | 
    210 | 	div.gsumxref {
    211 | 	  /* grammar summary cross-reference, used in Annex A */
    212 | 	  width: 8em;
    213 | 	  float: right;
    214 | 	}
    215 | 
    216 | 	span.prod {
    217 | 	  margin-left: 5pt;
    218 | 	  margin-right: 5pt;
    219 | 	}
    220 | 
    221 | 	div.rhs > code.t,
    222 | 	div.rhs > span.nt,
    223 | 	div.rhs > span.grhsannot,
    224 | 	div.rhs > span.grhsmod,
    225 | 	div.rhs > span.chgloss,
    226 | 	div.rhs > span.gprose,
    227 | 	div.rhs > var,
    228 | 	.prod > span.geq,
    229 | 	.prod > code.t,
    230 | 	.prod > span.nt,
    231 | 	.prod > span.grhsannot,
    232 | 	.prod > span.grhsmod,
    233 | 	.prod > span.chgloss,
    234 | 	.prod > span.gprose,
    235 | 	div.rhs > var {
    236 | 	  margin-left: 5pt;
    237 | 	}
    238 | 
    239 | 	div.rhs > code.t:first-child,
    240 | 	div.rhs > span.nt:first-child,
    241 | 	div.rhs > span.grhsannot:first-child,
    242 | 	div.rhs > span.grhsmod:first-child,
    243 | 	div.rhs > span.chgloss:first-child,
    244 | 	div.rhs > span.gprose:first-child,
    245 | 	div.rhs > var:first-child,
    246 | 	.prod > span.geq:first-child,
    247 | 	.prod > code.t:first-child,
    248 | 	.prod > span.nt:first-child,
    249 | 	.prod > span.grhsannot:first-child,
    250 | 	.prod > span.grhsmod:first-child,
    251 | 	.prod > span.chprose:first-child,
    252 | 	.prod > span.gprose:first-child,
    253 | 	.prod > var:first-child {
    254 | 	  margin-left: 0;
    255 | 	}
    256 | 
    257 | 	ul > li {
    258 | 	  list-style-type: disc;
    259 | 	}
    260 | 
    261 | 	ul > li > p {
    262 | 	  margin-bottom: 0.25em;
    263 | 	  margin-top: 0em;
    264 | 	}
    265 | 
    266 | 	ol.proc {
    267 | 	  margin-top: 0.5em;
    268 | 	}
    269 | 
    270 | 	ol.proc > li {
    271 | 	  list-style-type: decimal;
    272 | 	}
    273 | 
    274 | 	ol.proc > li > ol.block > li {
    275 | 	  list-style-type: lower-latin;
    276 | 	}
    277 | 
    278 | 	.tab-content
    279 | 	ol.proc > li > ol.block > li > ol.block > li {
    280 | 	  list-style-type: lower-roman;
    281 | 	}
    282 | 
    283 | 	ol.proc > li > ol.block > li > ol.block > li >
    284 | 	ol.block > li {
    285 | 	  list-style-type: decimal;
    286 | 	}
    287 | 
    288 | 	ol.proc > li > ol.block > li > ol.block > li >
    289 | 	ol.block > li > ol.block > li {
    290 | 	  list-style-type: lower-latin;
    291 | 	}
    292 | 
    293 | 	.tab-content
    294 | 	ol.proc > li > ol.block > li > ol.block > li >
    295 | 	ol.block > li > ol.block > li > ol.block > li {
    296 | 	  list-style-type: lower-roman;
    297 | 	}
    298 | 
    299 | 	p.special1 {
    300 | 	  padding-left: +3em;
    301 | 	  text-indent: -1em;
    302 | 	}
    303 | 
    304 | 	p.special2 {
    305 | 	  padding-left: +11em;
    306 | 	  text-indent: -1em;
    307 | 	}
    308 | 
    309 | 	p.special3 {
    310 | 	  padding-left: +6em;
    311 | 	  text-indent: -1em;
    312 | 	}
    313 | 
    314 | 	p.special4 {
    315 | 	  padding-left: +3em;
    316 | 	  text-indent: -1em;
    317 | 	}
    318 | }
    
    
    --------------------------------------------------------------------------------
    /src/spec.js:
    --------------------------------------------------------------------------------
      1 | import 'fetch';
      2 | import * as Utils from './mod/utils';
      3 | import {domconsole} from './mod/domconsole';
      4 | import {config} from './config';
      5 | // storage stuff
      6 | 
      7 | export let Store = localforage.createInstance({
      8 | 	name: 'especser',
      9 | 	storeName: config.IDBstoreName
     10 | });
     11 | 
     12 | export let Data = {
     13 | 	indexToId: {}, // 4.6.5: #sec-foo
     14 | 	idToIndex: {}, // #sec-foo: 4.6.5...
     15 | 	indexToFrameIndex: {}, // eg: 2.1.3 maps to 17 where 17 is the index of the corresponding frame inside stack
     16 | 	stack: [] // contains frames containing data
     17 | };
     18 | 
     19 | export function indexToPath (index) {
     20 | 	let pathNums = index.split('.');
     21 | 	return pathNums.map((_, i, arr) => arr.slice(0, i + 1).join('.')).map(
     22 | 		num => Data.stack[Data.indexToFrameIndex[num]].title
     23 | 	).join(' | ');
     24 | }
     25 | 
     26 | export function indexToFrame (index) {
     27 | 	try {
     28 | 		return Data.stack[Data.indexToFrameIndex[index]];
     29 | 	} catch (err) { return null; }
     30 | }
     31 | 
     32 | // functions to scrape spec
     33 | 
     34 | function fetchSpec (url = config.SPEC_URL) {
     35 | 	console.log('sending request to %s', url);
     36 | 	let f = fetch(
     37 | 		url
     38 | 	).then(res => res.text())
     39 | 	.then(html => new DOMParser().parseFromString(html, 'text/html'));
     40 | 	return f;
     41 | }
     42 | 
     43 | // replaces multiple simultaneous whitespace characters with single space
     44 | function normalize (string) {
     45 | 	return string.replace(/(?:\n+|\s+)/g, ' ');
     46 | }
     47 | 
     48 | // *internal* takes list element, secnum (whatever) and extracts a title value
     49 | function extractText (el, secnum) {
     50 | 	let c = $.cl('toc', el);
     51 | 	let title = '';
     52 | 	let nextEl = secnum.nextSibling;
     53 | 	while (nextEl && nextEl.nodeName.toLowerCase() !== 'ol') {
     54 | 		title += nextEl.textContent;
     55 | 		nextEl = nextEl.nextSibling;
     56 | 	}
     57 | 	return normalize(title.trim());
     58 | }
     59 | 
     60 | function parseIndex (doc) {
     61 | 	let elements = $$('span.secnum[id^="sec-"]', doc);
     62 | 
     63 | 	elements.forEach(function (secnum, stackIndex) {
     64 | 
     65 | 		let index = secnum.textContent;
     66 | 		let isAnnex = false;
     67 | 		if (index.startsWith('Annex')) {
     68 | 			index = index.replace('Annex', '').trim();
     69 | 			isAnnex = true;
     70 | 		}
     71 | 
     72 | 		let path = index.split('.');
     73 | 		path = path.reduce(function (path, place) {
     74 | 			let curr = path[path.length - 1];
     75 | 			path.push(curr + '.' + place);
     76 | 			return path;
     77 | 		}, [path.shift()]);
     78 | 
     79 | 		let id = secnum.firstChild.getAttribute('href').replace('#', '');
     80 | 
     81 | 		let title = secnum.parentNode.textContent;
     82 | 
     83 | 		if (isAnnex) {
     84 | 			title = title.replace('Annex ' + index, '').trim();
     85 | 		}
     86 | 		else {
     87 | 			title = title.replace(index, '').trim();
     88 | 		}
     89 | 
     90 | 		let children = [];
     91 | 		let def = {index, id, title, children, path, stackIndex};
     92 | 		Data.stack.push(def);
     93 | 
     94 | 		Data.indexToId[index] = id;
     95 | 		Data.idToIndex[id] = index;
     96 | 		Data.indexToFrameIndex[index] = stackIndex;
     97 | 
     98 | 		let parent = path[path.length - 2];
     99 | 		if (parent) {
    100 | 			Data.stack[Data.indexToFrameIndex[parent]].children.push(index);
    101 | 		}
    102 | 
    103 | 	});
    104 | 
    105 | 	return doc;
    106 | 
    107 | }
    108 | 
    109 | function processStack (doc) {
    110 | 	console.log('starting stack processing of %s frames', Data.stack.length);
    111 | 	return Promise.all(Data.stack.map( // we have to defer this because if #8.5 refers to #9.5, the index will not be found
    112 | 		frame => Store.setItem(frame.index, extractMaterial(frame.id, doc))
    113 | 	)).then(
    114 | 		_ => Store.setItem('appdata', Data)
    115 | 	);
    116 | }
    117 | 
    118 | // *internal* conditionally assigns data-index attribute to element and returns modified element
    119 | function assignIndex (el) {
    120 | 	let id = el.getAttribute('href');
    121 | 	if (!id || !id.startsWith('#')) return el;
    122 | 	let index = Data.idToIndex[id.replace('#', '')];
    123 | 	if (!index) return el;
    124 | 	el.setAttribute('href', '#' + index);
    125 | 	el.dataset.index = index;
    126 | 	el.classList.add('link-newtab');
    127 | 	return el;
    128 | }
    129 | 
    130 | function extractMaterial (hash, content) {
    131 | 	let c = $.id(hash.replace('#', ''), content);
    132 | 	let f = $.cl('front', c);
    133 | 	let container = f || c;
    134 | 	let clone = container.cloneNode(true);
    135 | 	$$.attr('id', undefined, clone).forEach(el => {el.removeAttribute('id');});
    136 | 	$$.attr('href^=', '#', clone).forEach(assignIndex);
    137 | 	return clone.innerHTML;
    138 | }
    139 | 
    140 | export function update () {
    141 | 	console.log('update started');
    142 | 	domconsole.log('fetching latest version of spec and caching locally. this might take a while.')
    143 | 	return fetchSpec().then(parseIndex).then(processStack).then(
    144 | 		_ => window.localStorage.setItem('lastIndexed', Date.now())
    145 | 	).then(
    146 | 		_ => {
    147 | 			console.log('stack processed and saved in indexeddb. marked lastindex in localstorage');
    148 | 			domconsole.log('caching and parsing completed! you can search for stuff and browse the spec now! :) (double click here to hide me)');
    149 | 			$.id('console').addEventListener('dblclick', function removeMe () {
    150 | 				this.classList.add('hidden');
    151 | 				this.removeEventListener('dblclick', removeMe);
    152 | 			}, false);
    153 | 			window.dispatchEvent(new Event('hashchange'));
    154 | 		}
    155 | 	);
    156 | }
    157 | 
    158 | export function initialize () {
    159 | 	console.log('initializing especser. we have ignition!');
    160 | 	if (window.localStorage.getItem('lastIndexed')) {
    161 | 		return Store.getItem('appdata').then(val => {
    162 | 			Data = val;
    163 | 			console.log('retrieved appdata from indexeddb from %s', localStorage.getItem('lastIndexed'));
    164 | 		});
    165 | 	}
    166 | 	console.log('this session is brand new. starting update threads!');
    167 | 	return update();
    168 | }
    169 | 
    170 | export function clear () {
    171 | 	console.log('I have got orders from high command to evacuate all data from the ship.');
    172 | 	domconsole.log('clearing store. this might take some time.');
    173 | 	Data = {
    174 | 		indexToId: {},
    175 | 		idToIndex: {},
    176 | 		indexToFrameIndex: {},
    177 | 		stack: []
    178 | 	};
    179 | 	Store.clear().then(_ => localStorage.removeItem('lastIndexed')).then(
    180 | 		_ => domconsole.log('store was emptied. click update to cache spec again.')
    181 | 	);
    182 | }
    183 | 
    184 | // spec usage API to be exposed
    185 | 
    186 | const MAX_RESULTS = 8;
    187 | 
    188 | // *internal* query to be found in name
    189 | function fuzzySearch (name, query, max = MAX_RESULTS) {
    190 | 	let pos = -1;
    191 | 	for (let i = 0, len = query.length; i < len; i++) {
    192 | 		let char = query[i];
    193 | 		if (!char.trim()) continue; // removing whitespace
    194 | 		pos = name.indexOf(char, pos+1);
    195 | 		if (pos === -1) return false;
    196 | 	}
    197 | 	return true;
    198 | }
    199 | 
    200 | const RE_SEC = /^sec:\s*(?:(?:\d+\.?)+,?\s*)+\b/i;
    201 | 
    202 | /*
    203 | 
    204 | ^ # start of string											|||||
    205 | sec: # "sec:"												|||||
    206 | \s* # forgive whitespace									|||||
    207 | (?: # begin matching set of indices					|||||		|||||
    208 | 	(?: # begin matching individaul indices		|||||	|||||		|||||
    209 | 		\d+ # one or more digits				|||||	|||||		|||||
    210 | 		\.? # followed by a period				|||||	|||||		|||||
    211 | 	)+ # and done								|||||	|||||		|||||
    212 | 	,? # and maybe commas							|||||		|||||
    213 | 	\s* # and maybe whitespace						|||||		|||||
    214 | )+ # 1 or more times								|||||		|||||
    215 | \b # and word boundary so trailing commas aren't matched	|||||
    216 | 
    217 | */
    218 | 
    219 | function* executeSearch (stack, query, max = MAX_RESULTS) {
    220 | 
    221 | 	query = (query + '').trim().toLowerCase();
    222 | 	if (!query) return [];
    223 | 
    224 | 	let index = query.match(RE_SEC), selectedStack = [];
    225 | 
    226 | 	if (index && index[0]) {
    227 | 		let indices = index[0].replace('sec:', '').trim().split(/,\s*/);
    228 | 
    229 | 		// initializing selectedStack with parent indices and all there children
    230 | 		// our stack is pretty large. so not using closures here
    231 | 		for (let i = 0; i < indices.length; i++) {
    232 | 			let frame = indexToFrame(indices[i]);
    233 | 			if (!frame) continue;
    234 | 			indices.push(...frame.children);
    235 | 			selectedStack.push(frame);
    236 | 		}
    237 | 		query = query.replace(RE_SEC, '').replace(/[.,]/g, '').trim();
    238 | 		if (!query) return selectedStack; // we return it here itself because the stack will likely not be large
    239 | 	} else {
    240 | 		selectedStack = stack;
    241 | 	}
    242 | 
    243 | 	for (
    244 | 		let i = 0, len = selectedStack.length,
    245 | 		results = [], fuzzyResults = [];
    246 | 		i < len;
    247 | 		i++
    248 | 	) {
    249 | 		let title = selectedStack[i].title.toLowerCase();
    250 | 		if (title.indexOf(query) >= 0) {
    251 | 			results.push(selectedStack[i]);
    252 | 		}
    253 | 		else if (fuzzySearch(title, query)) {
    254 | 			fuzzyResults.push(selectedStack[i]);
    255 | 		}
    256 | 		if (results.length >= max) {
    257 | 			yield results;
    258 | 			results = [];
    259 | 			continue;
    260 | 		}
    261 | 		else if (i >= len - 1) {
    262 | 			results.push(...fuzzyResults.slice(0, (max - results.length) - 1));
    263 | 			yield results;
    264 | 			results = [];
    265 | 			continue;
    266 | 		}
    267 | 	}
    268 | 	return [];
    269 | 
    270 | }
    271 | 
    272 | export function search (query) {
    273 | 	return executeSearch(Data.stack, query);
    274 | };
    275 | 
    
    
    --------------------------------------------------------------------------------
    /src/domhandler.js:
    --------------------------------------------------------------------------------
      1 | import * as Spec from './spec';
      2 | import * as Shortcuts from './mod/shortcuts';
      3 | import * as Utils from './mod/utils';
      4 | import {domconsole} from './mod/domconsole';
      5 | 
      6 | /**
      7 |  * helpers for handlers
      8 |  */
      9 | 
     10 | function emphasizeSearch (search, text) {
     11 | 	return text.replace(RegExp(Utils.re.escape(search), 'gi'), '$&');
     12 | }
     13 | 
     14 | /**
     15 |  * event handlers
     16 |  */
     17 | 
     18 | // a live nodelist increases performance in the following event handlers
     19 | let results = document.getElementById('search-results').getElementsByClassName('result');
     20 | let inputBox = $.nam('search');
     21 | let active = inputBox;
     22 | let resultsGenerator;
     23 | let resultsBox = $.id('search-results');
     24 | 
     25 | function createResultList (results, val) {
     26 | 	return results.map(res => $.make('li', {
     27 | 			childNodes: [$.make('a', {
     28 | 				classList: ['result', 'link-newtab'],
     29 | 				href: `#${encodeURIComponent(res.index)}`,
     30 | 				dataset: {
     31 | 					index: res.index
     32 | 				},
     33 | 				on: {
     34 | 					click: function (e) {
     35 | 						e.preventDefault();
     36 | 						active.classList.remove('active');
     37 | 						this.classList.add('active');
     38 | 						active = this;
     39 | 						form$onEnter.call($.id('search-form'), {target: inputBox});
     40 | 					}
     41 | 				},
     42 | 				childNodes: [
     43 | 					$.make('h4', {
     44 | 						classList: ['result-heading'],
     45 | 						innerHTML: emphasizeSearch(val, res.title)
     46 | 					}),
     47 | 					$.make('span', {
     48 | 						classList: ['result-index'],
     49 | 						textContent: res.index
     50 | 					}),
     51 | 					$.make('span', {
     52 | 						classList: ['result-path'],
     53 | 						textContent: Spec.indexToPath(res.index)
     54 | 					})
     55 | 				]
     56 | 			})]
     57 | 		}));
     58 | }
     59 | 
     60 | function input$onInput () {
     61 | 	let val = this.value.trim();
     62 | 	resultsGenerator = Spec.search(val);
     63 | 	active = inputBox;
     64 | 
     65 | 	let resultList = createResultList(resultsGenerator.next().value, val);
     66 | 
     67 | 	$$.tag('li', resultsBox).forEach($.remove);
     68 | 
     69 | 	resultList.forEach(el => $.append(el, resultsBox));
     70 | }
     71 | 
     72 | function form$onKeyDown (ev) {
     73 | 	const DOWN_ARROW = 40;
     74 | 	const UP_ARROW = 38;
     75 | 	const ENTER_KEY = 13;
     76 | 	const ESC_KEY = 27;
     77 | 
     78 | 	switch (ev.keyCode || ev.which) {
     79 | 		case DOWN_ARROW:
     80 | 			ev.preventDefault();
     81 | 			form$onDownArrow.call(this);
     82 | 			break;
     83 | 		case UP_ARROW:
     84 | 			ev.preventDefault();
     85 | 			form$onUpArrow.call(this);
     86 | 			break;
     87 | 		case ENTER_KEY:
     88 | 			ev.preventDefault();
     89 | 			ev.stopImmediatePropagation();
     90 | 			form$onEnter.call(this, ev);
     91 | 			break;
     92 | 		case ESC_KEY:
     93 | 			ev.preventDefault();
     94 | 			ev.stopImmediatePropagation();
     95 | 			form$onEscape.call(this);
     96 | 			break;
     97 | 	}
     98 | }
     99 | 
    100 | function form$onDownArrow () {
    101 | 	if (active === inputBox) {
    102 | 		return results[0] && simulateFocus(results[0]);
    103 | 	}
    104 | 	if (active.classList.contains('result')) {
    105 | 		if (active === resultsBox.lastElementChild.firstElementChild) {
    106 | 			let moreResults = resultsGenerator.next();
    107 | 			if (moreResults.done) return;
    108 | 			if (moreResults.value.length) {
    109 | 				let resultList = createResultList(moreResults.value, inputBox.value.trim());
    110 | 				resultList.forEach(el => $.append(el, resultsBox));
    111 | 				simulateFocus(resultList[0].firstChild);
    112 | 				return;
    113 | 			}
    114 | 			return;
    115 | 		}
    116 | 		try {
    117 | 			return simulateFocus(active.parentNode.nextElementSibling.firstChild);
    118 | 		} catch (er) {}
    119 | 	}
    120 | }
    121 | 
    122 | function form$onUpArrow () {
    123 | 	if (active === inputBox) {
    124 | 		let lastResult = results[results.length - 1];
    125 | 		if (lastResult) {
    126 | 			return simulateFocus(lastResult);
    127 | 		}
    128 | 		else return;
    129 | 	}
    130 | 	if (active.classList.contains('result')) {
    131 | 		if (active === results[0]) {
    132 | 			return;
    133 | 		}
    134 | 		try {
    135 | 			return simulateFocus(active.parentNode.previousElementSibling.firstChild);
    136 | 		} catch (err) {}
    137 | 	}
    138 | }
    139 | 
    140 | function form$onEnter (ev) {
    141 | 	let target = ev.target || ev.srcElement;
    142 | 	if (target !== inputBox) return;
    143 | 
    144 | 	if (!active.classList.contains('result')) return;
    145 | 	target = active;
    146 | 	openTab(target.dataset.index);
    147 | 	form$onEscape.call(this);
    148 | }
    149 | 
    150 | function form$onEscape () {
    151 | 	this.parentNode.classList.add('hidden');
    152 | }
    153 | 
    154 | function simulateFocus (el) {
    155 | 	active.classList.remove('active');
    156 | 	el.classList.add('active');
    157 | 	active = el;
    158 | 	el.scrollIntoView();
    159 | }
    160 | 
    161 | function window$loaded (ev) {
    162 | 	if (window.localStorage.getItem('lastIndexed')) {
    163 | 		Spec.initialize().then(_ => app$navigated.call(this, ev));
    164 | 	}
    165 | 	else {
    166 | 		domconsole.log('hi! especser is an app to search the ECMAScript specification ed6.0. please click update to cache spec for the first time.');
    167 | 	}
    168 | }
    169 | 
    170 | function app$navigated (ev) {
    171 | 	let newIndex = window.location.hash.replace('#', '').trim();
    172 | 	let frame = Spec.indexToFrame(newIndex);
    173 | 	if (frame) {
    174 | 		Promise.resolve(openTab(newIndex)).then(_ => $.id('top-bar').classList.add('hidden'));
    175 | 	}
    176 | }
    177 | 
    178 | /**
    179 |  * attach the above awesomeness to dom!
    180 |  */
    181 | 
    182 | inputBox.addEventListener('input', Utils.throttle(input$onInput, 200, inputBox, 'discard-repeats'), true);
    183 | $.id('search-form').addEventListener('keydown', form$onKeyDown, false);
    184 | $.id('btn-update').addEventListener('click', Spec.initialize, false);
    185 | $.id('btn-clear').addEventListener('click', Spec.clear, false);
    186 | window.addEventListener('hashchange', app$navigated, false);
    187 | window.addEventListener('load', window$loaded);
    188 | 
    189 | /**
    190 |  * tab creation, previewing, and handling
    191 |  */
    192 | 
    193 | import {TabGroup, Tab} from './mod/tabbing';
    194 | 
    195 | let group = new TabGroup();
    196 | 
    197 | let content = $.id('content');
    198 | let suspendedTabs = $.id('suspended-tabs');
    199 | let descriptorList = $.id('open-tab-descriptors');
    200 | 
    201 | let state = {
    202 | 	open: Object.create(null),
    203 | 	tokenToIndexMap: Object.create(null),
    204 | 	get activeTabIndex () {
    205 | 		let el = $.cl('tab-content', content);
    206 | 		if (!el) return;
    207 | 		return el.dataset.secIndex;
    208 | 	}
    209 | };
    210 | 
    211 | function openTab (index) {
    212 | 	if (index in state.open) {
    213 | 		return group.open(state.open[index]);
    214 | 	}
    215 | 
    216 | 	let tabData = Spec.indexToFrame(index);
    217 | 
    218 | 	let descriptor = createDescriptor(tabData);
    219 | 	descriptorList.appendChild(descriptor);
    220 | 	descriptor.scrollIntoView();
    221 | 
    222 | 	let tab = new Tab(tabData.title, tabData.path, index);
    223 | 	let tabToken = group.attach(tab);
    224 | 
    225 | 	state.open[index] = tabToken;
    226 | 	state.tokenToIndexMap[tabToken] = index;
    227 | }
    228 | 
    229 | function createDescriptor (res) {
    230 | 	return $.make('li', {
    231 | 		childNodes: [
    232 | 			$.make('div', {
    233 | 				classList: ['tab-descriptor', 'link-tab-descriptor', 'active'],
    234 | 				childNodes: [
    235 | 					$.make('a', {
    236 | 						textContent: res.title,
    237 | 						href: `#${res.index}`,
    238 | 						dataset: {
    239 | 							index: res.index
    240 | 						},
    241 | 						classList: ['link-tab-activate']
    242 | 					}),
    243 | 					$.make('span', {
    244 | 						classList: ['tab-close'],
    245 | 						on: {
    246 | 							click: function () {
    247 | 								group.detach(state.open[res.index]);
    248 | 								$.remove(this.parentNode.parentNode);
    249 | 							}
    250 | 						}
    251 | 					})
    252 | 				]
    253 | 			})
    254 | 		]
    255 | 	});
    256 | }
    257 | 
    258 | function closeTab (index) {
    259 | 	if (!(index in state.open)) {
    260 | 		return;
    261 | 	}
    262 | 	group.detach(state.open[index]);
    263 | 	delete state.tokenToIndexMap[token];
    264 | 	delete state.open[index];
    265 | }
    266 | 
    267 | function storeAsSuspended (index, data) {
    268 | 	if (!data) return;
    269 | 	$.remove(data);
    270 | 	$.apply(data, {
    271 | 		classList: ['tab-content'],
    272 | 		dataset: {
    273 | 			secIndex: index
    274 | 		}
    275 | 	});
    276 | 	suspendedTabs.appendChild(data);
    277 | }
    278 | 
    279 | function getDescriptor (index) {
    280 | 	return $.data('index=', index, descriptorList).parentNode.parentNode;
    281 | }
    282 | 
    283 | function suspendActiveTab () {
    284 | 	let index = state.activeTabIndex;
    285 | 	if (!index) return;
    286 | 	let data = $.data('sec-index=', index, content);
    287 | 	storeAsSuspended(index, data);
    288 | }
    289 | 
    290 | function getFromSuspended (index) {
    291 | 	return $.data('sec-index=', index, suspendedTabs);
    292 | }
    293 | 
    294 | function onAttach ({id: token}) {
    295 | 	let index = group.tabs[token].meta;
    296 | 
    297 | 	let el = generateView(Spec.indexToFrame(index));
    298 | 	el.then(data => storeAsSuspended(index, data)).then(_ => group.open(token));
    299 | }
    300 | 
    301 | function onOpen ({id: token}) {
    302 | 	let index = group.tabs[token].meta;
    303 | 	suspendActiveTab();
    304 | 	content.appendChild(getFromSuspended(index));
    305 | 	$.cl('tab-descriptor', getDescriptor(index)).classList.add('active');
    306 | }
    307 | 
    308 | function onClose ({id: token}) {
    309 | 	suspendActiveTab();
    310 | 	$.cl('active', descriptorList).classList.remove('active');
    311 | }
    312 | 
    313 | function onDetach ({id: token}) {
    314 | 	$.remove(getFromSuspended(state.tokenToIndexMap[token]));
    315 | 	group.restoreLast();
    316 | }
    317 | 
    318 | group.events.on('open', onOpen);
    319 | group.events.on('close', onClose);
    320 | group.events.on('attach', onAttach);
    321 | group.events.on('detach', onDetach);
    322 | 
    323 | function generateView (res) {
    324 | 	let path = Spec.indexToPath(res.index);
    325 | 	let info = $.make('div', {
    326 | 		classList: ['__info'],
    327 | 		childNodes: [
    328 | 			$.make('span', {classList: ['__info-label'], textContent: 'Path till here'}),
    329 | 			$.make('h4', {
    330 | 				classList: ['__info-path'],
    331 | 				childNodes: [...res.path.map(place => $.make('a', {
    332 | 					href: `#${place}`,
    333 | 					textContent: Spec.indexToFrame(place).title
    334 | 				}))]
    335 | 			})
    336 | 		]
    337 | 	});
    338 | 	let children = $.make('div', {
    339 | 		classList: ['__info'],
    340 | 		childNodes: [
    341 | 			$.make('span', {classList: ['__info-label'], textContent: 'Topics inside'}),
    342 | 			$.make('h4', {
    343 | 				classList: ['__info-children'],
    344 | 				childNodes: res.children.length ? [...res.children.map(child => $.make('div', {
    345 | 					childNodes: [
    346 | 						$.make('span', {
    347 | 							classList: ['__info-children-index'],
    348 | 							textContent: child
    349 | 						}),
    350 | 						$.make('a', {
    351 | 							classList: ['__info-children-anchor'],
    352 | 							href: `#${child}`,
    353 | 							textContent: Spec.indexToFrame(child).title
    354 | 						})
    355 | 					]
    356 | 				}))] : [$.make('span', {textContent: 'none'})]
    357 | 			})
    358 | 		]
    359 | 	});
    360 | 	let content = $.make('div', {
    361 | 		classList: ['tab-content'],
    362 | 		childNodes: [info]
    363 | 	});
    364 | 	return Spec.Store.getItem(res.index).then(
    365 | 		html => {
    366 | 			content.insertAdjacentHTML('beforeend', html);
    367 | 			content.appendChild(children);
    368 | 			return content;
    369 | 		}
    370 | 	);
    371 | }
    372 | 
    
    
    --------------------------------------------------------------------------------
    /src/mod/localforage.min.js:
    --------------------------------------------------------------------------------
    1 | /*!
    2 |     localForage -- Offline Storage, Improved
    3 |     Version 1.2.2
    4 |     https://mozilla.github.io/localForage
    5 |     (c) 2013-2015 Mozilla, Apache License 2.0
    6 | */
    7 | !function(){var a,b,c,d;!function(){var e={},f={};a=function(a,b,c){e[a]={deps:b,callback:c}},d=c=b=function(a){function c(b){if("."!==b.charAt(0))return b;for(var c=b.split("/"),d=a.split("/").slice(0,-1),e=0,f=c.length;f>e;e++){var g=c[e];if(".."===g)d.pop();else{if("."===g)continue;d.push(g)}}return d.join("/")}if(d._eak_seen=e,f[a])return f[a];if(f[a]={},!e[a])throw new Error("Could not find module "+a);for(var g,h=e[a],i=h.deps,j=h.callback,k=[],l=0,m=i.length;m>l;l++)k.push("exports"===i[l]?g={}:b(c(i[l])));var n=j.apply(this,k);return f[a]=g||n}}(),a("promise/all",["./utils","exports"],function(a,b){"use strict";function c(a){var b=this;if(!d(a))throw new TypeError("You must pass an array to all.");return new b(function(b,c){function d(a){return function(b){f(a,b)}}function f(a,c){h[a]=c,0===--i&&b(h)}var g,h=[],i=a.length;0===i&&b([]);for(var j=0;jb;b+=4)c=e.indexOf(a[b]),d=e.indexOf(a[b+1]),f=e.indexOf(a[b+2]),g=e.indexOf(a[b+3]),l[j++]=c<<2|d>>4,l[j++]=(15&d)<<4|f>>2,l[j++]=(3&f)<<6|63&g;return k}function d(a){var b,c=new Uint8Array(a),d="";for(b=0;b>2],d+=e[(3&c[b])<<4|c[b+1]>>4],d+=e[(15&c[b+1])<<2|c[b+2]>>6],d+=e[63&c[b+2]];return c.length%3===2?d=d.substring(0,d.length-1)+"=":c.length%3===1&&(d=d.substring(0,d.length-2)+"=="),d}var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",f="__lfsc__:",g=f.length,h="arbf",i="blob",j="si08",k="ui08",l="uic8",m="si16",n="si32",o="ur16",p="ui32",q="fl32",r="fl64",s=g+h.length,t={serialize:a,deserialize:b,stringToBuffer:c,bufferToString:d};"undefined"!=typeof module&&module.exports?module.exports=t:"function"==typeof define&&define.amd?define("localforageSerializer",function(){return t}):this.localforageSerializer=t}.call(window),function(){"use strict";function a(a){var b=this,c={db:null};if(a)for(var d in a)c[d]=a[d];return new k(function(a,d){var e=l.open(c.name,c.version);e.onerror=function(){d(e.error)},e.onupgradeneeded=function(){e.result.createObjectStore(c.storeName)},e.onsuccess=function(){c.db=e.result,b._dbInfo=c,a()}})}function b(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=f.get(a);g.onsuccess=function(){var a=g.result;void 0===a&&(a=null),b(a)},g.onerror=function(){d(g.error)}})["catch"](d)});return j(d,b),d}function c(a,b){var c=this,d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=f.openCursor(),h=1;g.onsuccess=function(){var c=g.result;if(c){var d=a(c.value,c.key,h++);void 0!==d?b(d):c["continue"]()}else b()},g.onerror=function(){d(g.error)}})["catch"](d)});return j(d,b),d}function d(a,b,c){var d=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var e=new k(function(c,e){d.ready().then(function(){var f=d._dbInfo,g=f.db.transaction(f.storeName,"readwrite"),h=g.objectStore(f.storeName);null===b&&(b=void 0);var i=h.put(b,a);g.oncomplete=function(){void 0===b&&(b=null),c(b)},g.onabort=g.onerror=function(){var a=i.error?i.error:i.transaction.error;e(a)}})["catch"](e)});return j(e,c),e}function e(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readwrite"),g=f.objectStore(e.storeName),h=g["delete"](a);f.oncomplete=function(){b()},f.onerror=function(){d(h.error)},f.onabort=function(){var a=h.error?h.error:h.transaction.error;d(a)}})["catch"](d)});return j(d,b),d}function f(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readwrite"),f=e.objectStore(d.storeName),g=f.clear();e.oncomplete=function(){a()},e.onabort=e.onerror=function(){var a=g.error?g.error:g.transaction.error;c(a)}})["catch"](c)});return j(c,a),c}function g(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readonly").objectStore(d.storeName),f=e.count();f.onsuccess=function(){a(f.result)},f.onerror=function(){c(f.error)}})["catch"](c)});return j(c,a),c}function h(a,b){var c=this,d=new k(function(b,d){return 0>a?void b(null):void c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=!1,h=f.openCursor();h.onsuccess=function(){var c=h.result;return c?void(0===a?b(c.key):g?b(c.key):(g=!0,c.advance(a))):void b(null)},h.onerror=function(){d(h.error)}})["catch"](d)});return j(d,b),d}function i(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readonly").objectStore(d.storeName),f=e.openCursor(),g=[];f.onsuccess=function(){var b=f.result;return b?(g.push(b.key),void b["continue"]()):void a(g)},f.onerror=function(){c(f.error)}})["catch"](c)});return j(c,a),c}function j(a,b){b&&a.then(function(a){b(null,a)},function(a){b(a)})}var k="undefined"!=typeof module&&module.exports?require("promise"):this.Promise,l=l||this.indexedDB||this.webkitIndexedDB||this.mozIndexedDB||this.OIndexedDB||this.msIndexedDB;if(l){var m={_driver:"asyncStorage",_initStorage:a,iterate:c,getItem:b,setItem:d,removeItem:e,clear:f,length:g,key:h,keys:i};"undefined"!=typeof module&&module.exports?module.exports=m:"function"==typeof define&&define.amd?define("asyncStorage",function(){return m}):this.asyncStorage=m}}.call(window),function(){"use strict";function a(a){var b=this,c={};if(a)for(var d in a)c[d]=a[d];c.keyPrefix=c.name+"/",b._dbInfo=c;var e=new k(function(a){q===p.DEFINE?require(["localforageSerializer"],a):a(q===p.EXPORT?require("./../utils/serializer"):l.localforageSerializer)});return e.then(function(a){return m=a,k.resolve()})}function b(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo.keyPrefix,c=n.length-1;c>=0;c--){var d=n.key(c);0===d.indexOf(a)&&n.removeItem(d)}});return j(c,a),c}function c(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=c.ready().then(function(){var b=c._dbInfo,d=n.getItem(b.keyPrefix+a);return d&&(d=m.deserialize(d)),d});return j(d,b),d}function d(a,b){var c=this,d=c.ready().then(function(){for(var b=c._dbInfo.keyPrefix,d=b.length,e=n.length,f=0;e>f;f++){var g=n.key(f),h=n.getItem(g);if(h&&(h=m.deserialize(h)),h=a(h,g.substring(d),f+1),void 0!==h)return h}});return j(d,b),d}function e(a,b){var c=this,d=c.ready().then(function(){var b,d=c._dbInfo;try{b=n.key(a)}catch(e){b=null}return b&&(b=b.substring(d.keyPrefix.length)),b});return j(d,b),d}function f(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo,c=n.length,d=[],e=0;c>e;e++)0===n.key(e).indexOf(a.keyPrefix)&&d.push(n.key(e).substring(a.keyPrefix.length));return d});return j(c,a),c}function g(a){var b=this,c=b.keys().then(function(a){return a.length});return j(c,a),c}function h(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=c.ready().then(function(){var b=c._dbInfo;n.removeItem(b.keyPrefix+a)});return j(d,b),d}function i(a,b,c){var d=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var e=d.ready().then(function(){void 0===b&&(b=null);var c=b;return new k(function(e,f){m.serialize(b,function(b,g){if(g)f(g);else try{var h=d._dbInfo;n.setItem(h.keyPrefix+a,b),e(c)}catch(i){("QuotaExceededError"===i.name||"NS_ERROR_DOM_QUOTA_REACHED"===i.name)&&f(i),f(i)}})})});return j(e,c),e}function j(a,b){b&&a.then(function(a){b(null,a)},function(a){b(a)})}var k="undefined"!=typeof module&&module.exports?require("promise"):this.Promise,l=this,m=null,n=null;try{if(!(this.localStorage&&"setItem"in this.localStorage))return;n=this.localStorage}catch(o){return}var p={DEFINE:1,EXPORT:2,WINDOW:3},q=p.WINDOW;"undefined"!=typeof module&&module.exports?q=p.EXPORT:"function"==typeof define&&define.amd&&(q=p.DEFINE);var r={_driver:"localStorageWrapper",_initStorage:a,iterate:d,getItem:c,setItem:i,removeItem:h,clear:b,length:g,key:e,keys:f};q===p.EXPORT?module.exports=r:q===p.DEFINE?define("localStorageWrapper",function(){return r}):this.localStorageWrapper=r}.call(window),function(){"use strict";function a(a){var b=this,c={db:null};if(a)for(var d in a)c[d]="string"!=typeof a[d]?a[d].toString():a[d];var e=new k(function(a){p===o.DEFINE?require(["localforageSerializer"],a):a(p===o.EXPORT?require("./../utils/serializer"):l.localforageSerializer)}),f=new k(function(d,e){try{c.db=n(c.name,String(c.version),c.description,c.size)}catch(f){return b.setDriver(b.LOCALSTORAGE).then(function(){return b._initStorage(a)}).then(d)["catch"](e)}c.db.transaction(function(a){a.executeSql("CREATE TABLE IF NOT EXISTS "+c.storeName+" (id INTEGER PRIMARY KEY, key unique, value)",[],function(){b._dbInfo=c,d()},function(a,b){e(b)})})});return e.then(function(a){return m=a,f})}function b(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT * FROM "+e.storeName+" WHERE key = ? LIMIT 1",[a],function(a,c){var d=c.rows.length?c.rows.item(0).value:null;d&&(d=m.deserialize(d)),b(d)},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function c(a,b){var c=this,d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT * FROM "+e.storeName,[],function(c,d){for(var e=d.rows,f=e.length,g=0;f>g;g++){var h=e.item(g),i=h.value;if(i&&(i=m.deserialize(i)),i=a(i,h.key,g+1),void 0!==i)return void b(i)}b()},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function d(a,b,c){var d=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var e=new k(function(c,e){d.ready().then(function(){void 0===b&&(b=null);var f=b;m.serialize(b,function(b,g){if(g)e(g);else{var h=d._dbInfo;h.db.transaction(function(d){d.executeSql("INSERT OR REPLACE INTO "+h.storeName+" (key, value) VALUES (?, ?)",[a,b],function(){c(f)},function(a,b){e(b)})},function(a){a.code===a.QUOTA_ERR&&e(a)})}})})["catch"](e)});return j(e,c),e}function e(a,b){var c=this;"string"!=typeof a&&(window.console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("DELETE FROM "+e.storeName+" WHERE key = ?",[a],function(){b()},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function f(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("DELETE FROM "+d.storeName,[],function(){a()},function(a,b){c(b)})})})["catch"](c)});return j(c,a),c}function g(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("SELECT COUNT(key) as c FROM "+d.storeName,[],function(b,c){var d=c.rows.item(0).c;a(d)},function(a,b){c(b)})})})["catch"](c)});return j(c,a),c}function h(a,b){var c=this,d=new k(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT key FROM "+e.storeName+" WHERE id = ? LIMIT 1",[a+1],function(a,c){var d=c.rows.length?c.rows.item(0).key:null;b(d)},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function i(a){var b=this,c=new k(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("SELECT key FROM "+d.storeName,[],function(b,c){for(var d=[],e=0;e