diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4e882bc7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*] +end_of_line = lf +trim_trailing_whitespace = true + +[*.js] +indent_style = space +indent_size = 4 diff --git a/.github/contributing.md b/.github/contributing.md new file mode 100644 index 00000000..313059cf --- /dev/null +++ b/.github/contributing.md @@ -0,0 +1,27 @@ +# Contributing + +There are a few steps to take to get twig.js building in your environment. + + +## Requirements + +In order work on twig.js you will need node installed to run the tests and create the minified version of twig.js + +## Building + +1. Fork and clone the twig.js git repository. +2. Run `npm ci` to install the development dependencies. +3. Make your changes to the source files in `src/`. +4. Add/update tests in `test/`. +5. Run `npm run test` to make sure your tests pass. +6. Run `npm run build` to build the source. + + +## Contributing + +1. If possible, create tests (in the `test/` directory) which test your changes. E.g. if you found and fixed a bug, create a test which fails in the buggy version. +2. Please commit only changes in the source code, not in the built files like `twig.js`, `twig.min.js`, etc. as they blow up the repository and create conflicts when merging pull requests. We build a final file when releasing a new version. +3. If possible, rebase your changes to the current master. +4. Tidy up your commit history. It's important to have distinct commits so that you can follow development process. +5. Push a branch to your fork on Github and create a pull request. + diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..49ef2f73 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,69 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: Deploy VitePress site to Pages + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [write-docs-using-vitepress] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Install dependencies + run: npm ci + working-directory: docs + + - name: Build with VitePress + run: npm run docs:build + working-directory: docs + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..03662771 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,41 @@ +name: "tests" + +on: + push: + pull_request: + +jobs: + tests: + name: "Node v${{ matrix.node_js }}" + + runs-on: "ubuntu-latest" + + strategy: + fail-fast: true + matrix: + node_js: + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + + steps: + - name: "Checkout code" + uses: "actions/checkout@v3" + + - name: "Setup Node and npm" + uses: "actions/setup-node@v3" + with: + cache: "npm" + node-version: "${{ matrix.node_js }}" + + - name: "Install Node dependencies" + run: "npm ci" + + - name: "Run tests" + run: "npm run test" diff --git a/.gitignore b/.gitignore index 94b06b78..28b2bd6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ -node_modules -.c9revisions -twig.php +.idea/ +node_modules/ +twig.js +twig.min.js +twig.min.js.map +twig.min.js.LICENSE.txt + +# docs +docs/.vitepress/cache/ +docs/.vitepress/dist/ diff --git a/.gitmodules b/.gitmodules index 17a914cc..fbdf6eb8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "docs/wiki"] path = docs/wiki - url = git://github.com/justjohn/twig.js.wiki.git + url = https://github.com/twigjs/twig.js.wiki.git diff --git a/.npmignore b/.npmignore index d479aa1e..8b8a027b 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,14 @@ demos tools test +test-ext +docs qtest -src .hg .git +.gitignore +.gitmodules +.travis.yml +.idea +webpack.config.js +bower.json diff --git a/.travis.yml b/.travis.yml index 8e3af8fb..75c38d3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,15 @@ language: node_js node_js: - - 0.6 - - 0.8 - - 0.10 - - 0.11 + - 10 + - 11 + - 12 + - 13 + +cache: + directories: + - node_modules + - $HOME/.npm + +notifications: + on_success: false + email: false diff --git a/ASYNC.md b/ASYNC.md new file mode 100644 index 00000000..989b8686 --- /dev/null +++ b/ASYNC.md @@ -0,0 +1,218 @@ +# Twig Asynchronous Rendering + +## Synchronous promises + +The asynchronous behaviour of Twig.js relies on promises, in order to support both the synchronous and asynchronous behaviour there is an internal promise implementation that runs fully synchronous. + +The internal implementation of promises does not use `setTimeout` to run through the promise chain, but instead synchronously runs through the promise chain. + +The different promise implementations can be mixed, synchronous behaviour however is no longer guaranteed as soon as the regular promise implementation is run. + +### Examples + +**Internal (synchronous) implementation** + +[Internal implementation](https://github.com/JorgenEvens/twig.js/tree/master/src/twig.async.js#L40) + +```javascript +console.log('start'); +Twig.Promise.resolve('1') +.then(function(v) { + console.log(v); + return '2'; +}) +.then(function(v) { + console.log(v); +}); +console.log('stop'); + +/** + * Prints to the console: + * start + * 1 + * 2 + * stop + */ +``` + +**Regular / native promises** + +Implementations such as the native promises or [bluebird](http://bluebirdjs.com/docs/getting-started.html) promises. + +```javascript +console.log('start'); +Promise.resolve('1') +.then(function(v) { + console.log(v); + return '2'; +}) +.then(function(v) { + console.log(v); +}); +console.log('stop'); + +/** + * Prints to the console: + * start + * stop + * 1 + * 2 + */ +``` + +**Mixing promises** + +```javascript +console.log('start'); +Twig.Promise.resolve('1') +.then(function(v) { + console.log(v); + return Promise.resolve('2'); +}) +.then(function(v) { + console.log(v); +}); +console.log('stop'); + +/** + * Prints to the console: + * start + * 1 + * stop + * 2 + */ +``` + + +## Async helpers + +To preserve the correct order of execution there is an implemenation of `Twig.forEach()` that waits any promises returned from the callback before executing the next iteration of the loop. If no promise is returned the next iteration is invoked immediately. + +```javascript +var arr = new Array(5); + +Twig.async.forEach(arr, function(value, index) { + console.log(index); + + if (index % 2 == 0) + return index; + + return Promise.resolve(index); +}) +.then(function() { + console.log('finished'); +}); + +/** + * Prints to the console: + * 0 + * 1 + * 2 + * 3 + * 4 + * finished + */ +``` + +## Switching render mode + +The rendering mode of Twig.js internally is determined by the `allowAsync` argument that can be passed into `Twig.expression.parse`, `Twig.logic.parse`, `Twig.parse` and `Twig.Template.render`. Detecting if at any point code runs asynchronously is explained in [detecting asynchronous behaviour](#detecting-asynchronous-behaviour). + +For the end user switching between synchronous and asynchronous is as simple as using a different method on the template instance. + +**Render template synchronously** + +```javascript +var output = twig({ + data: 'a {{value}}' +}).render({ + value: 'test' +}); + +/** + * Prints to the console: + * a test + */ +``` + +**Render template asynchronously** + +```javascript +var template = twig({ + data: 'a {{value}}' +}).renderAsync({ + value: 'test' +}) +.then(function(output) { + console.log(output); +}); + +/** + * Prints to the console: + * a test + */ +``` + +## Detecting asynchronous behaviour + +The pattern used to detect asynchronous behaviour is the same everywhere it is used and follows a simple pattern. + +1. Set a variable `isAsync = true` +2. Run the promise chain that might contain some asynchronous behaviour. +3. As the last method in the promise chain set `isAsync = false` +4. Underneath the promise chain test whether `isAsync` is `true` + +This pattern works because the last method in the chain will be executed in the next run of the eventloop (`setTimeout`/`setImmediate`). + +### Examples + +**Synchronous promises only** + +```javascript +var isAsync = true; + +Twig.Promise.resolve() +.then(function() { + // We run our work in here such to allow for asynchronous work + // This example is fully synchronous + return 'hello world'; +}) +.then(function() { + isAsync = false; +}); + +if (isAsync) + console.log('method ran asynchronous'); + +console.log('method ran synchronous'); + +/** + * Prints to the console: + * method ran synchronous + */ +``` + +**Mixed promises** + +```javascript +var isAsync = true; + +Twig.Promise.resolve() +.then(function() { + // We run our work in here such to allow for asynchronous work + return Promise.resolve('hello world'); +}) +.then(function() { + isAsync = false; +}); + +if (isAsync) + console.log('method ran asynchronous'); + +console.log('method ran synchronous'); + +/** + * Prints to the console: + * method ran asynchronous + */ +``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..1dd062c2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,352 @@ +Version 1.17.0, release 2023-11-16 +---------------------------------- +Major improvements: +* Add string position of tokens in token trees by @synga-nl in https://github.com/twigjs/twig.js/pull/859 +* Allow multiple spaces after elseif statement. by @antoineveldhoven in https://github.com/twigjs/twig.js/pull/870 +* Make is empty return false for boolean true. by @antoineveldhoven in https://github.com/twigjs/twig.js/pull/869 +* Add support for spaceship operator by @antoineveldhoven in https://github.com/twigjs/twig.js/pull/873 +* Allow colon inside Twig.expression.type.key.brackets. by @antoineveldhoven in https://github.com/twigjs/twig.js/pull/879 +* Support variables in slice filter shorthand by @antoineveldhoven in https://github.com/twigjs/twig.js/pull/881 + +Minor improvements: +* Bump @babel/traverse from 7.12.5 to 7.23.2 by @dependabot in https://github.com/twigjs/twig.js/pull/877 + +Version 1.16.0, release 2023-02-27 +---------------------------------- +Major improvements: +* Fix passing context around by @willrowe in https://github.com/twigjs/twig.js/pull/850 +* Add namespace support to `source` function by @willrowe in https://github.com/twigjs/twig.js/pull/823 +* Use src/twig.js as package's main script instead of the compiled twig.js by @RobLoach in https://github.com/twigjs/twig.js/pull/829 + +Minor improvements: +* Fix macro changing context in loop by @mihkeleidast in https://github.com/twigjs/twig.js/pull/773 +* Imported function PATHS.strip_slash() missing by @murageyun in https://github.com/twigjs/twig.js/pull/770 +* Convert non-string values to string before replacing by @kmonahan in https://github.com/twigjs/twig.js/pull/797 +* Add GitHub actions test workflow by @willrowe in https://github.com/twigjs/twig.js/pull/817 +* Fix date parsing with timezones by @plepe in https://github.com/twigjs/twig.js/pull/765 +* Fixed Twig official's URL on README.md by @Geolim4 in https://github.com/twigjs/twig.js/pull/822 +* Add tests for whitespace in paths by @willrowe in https://github.com/twigjs/twig.js/pull/824 +* Fix multiple includes with embeds by @willrowe in https://github.com/twigjs/twig.js/pull/828 +* Update to Mocha 9.x by @RobLoach in https://github.com/twigjs/twig.js/pull/831 +* Add test for issue #767 by @willrowe in https://github.com/twigjs/twig.js/pull/837 +* Add support for `divisible by` test by @willrowe in https://github.com/twigjs/twig.js/pull/838 +* Add support for `with` tag without context or `only` keyword by @willrowe in https://github.com/twigjs/twig.js/pull/839 +* Use v3 of `actions/checkout` by @willrowe in https://github.com/twigjs/twig.js/pull/846 +* Test on more node versions by @willrowe in https://github.com/twigjs/twig.js/pull/847 +* Fix webpack 5 compatibility by @willrowe in https://github.com/twigjs/twig.js/pull/849 +* Add test to confirm `renderFile` error handling by @willrowe in https://github.com/twigjs/twig.js/pull/851 +* Fix casing of variables in docs by @willrowe in https://github.com/twigjs/twig.js/pull/852 +* Bumped dependencies by @dependabot + +Version 1.15.4, released 2020-11-27 +----------------------------------- +Minor improvements: +* Fix lost context when calling a macro multiple times ([#727](https://github.com/twigjs/twig.js/pull/727)) by [mihkeleidast ](https://github.com/mihkeleidast) + +Version 1.15.3, released 2020-11-05 +----------------------------------- +Minor improvements: +* Fix documentation of browser usage ([#755](https://github.com/twigjs/twig.js/pull/755)) by [odebparla](https://github.com/obedparla) +* Add support for template arrays when using extends ([#754](https://github.com/twigjs/twig.js/pull/754)) by [justafish](https://github.com/justafish) + +Version 1.15.2, released 2020-08-19 +----------------------------------- +Minor improvements: +* Specify MimeType to always use for AJAX templates ([#742](https://github.com/twigjs/twig.js/pull/742)) by [MasterOdin](https://github.com/MasterOdin) +* Added token count validation ([#745](https://github.com/twigjs/twig.js/pull/742)) by [HakS](https://github.com/haks) +* Async renderFile error callback ([#748](https://github.com/twigjs/twig.js/pull/748)) by [ArnauMrJeff](https://github.com/ArnauMrJeff) +* Ternary operator overrides context fix ([#737](https://github.com/twigjs/twig.js/issues/737)) by [oleg-andreyev](https://github.com/oleg-andreyev) +* Update lodash to `4.17.19` +* Update elliptic to `6.5.3` + +Version 1.15.1, released 2020-04-16 +----------------------------------- +Major improvements: +* Make "js" escaped strings embeddable in JSON ([#724](https://github.com/twigjs/twig.js/pull/724) by [@dorian-marchal]) + +Minor improvements: +* Fix parsing expression when value is `null` ([#735](https://github.com/twigjs/twig.js/pull/735) by [@RobLoach]) + +Version 1.15.0, released 2020-02-20 +----------------------------------- +Major improvements: +* Add support for arrays with `include` ([#681](https://github.com/twigjs/twig.js/pull/681) by [@justafish](https://github.com/justafish)) +* Add babel preset on serverBuild ([#707](https://github.com/twigjs/twig.js/pull/707) by [@stephane-r](https://github.com/stephane-r)) +* Support for "do" tag ([#703](https://github.com/twigjs/twig.js/pull/703) by [@drzraf](https://github.com/drzraf)) +* Update [`xo`](https://www.npmjs.com/package/xo) and code syntax by [@RobLoach](https://github.com/robloach) +* Deprecate Node.js 8 from testing by [@RobLoach](https://github.com/robloach) +* Support for Source Maps ([#700](https://github.com/twigjs/twig.js/pull/700) by [@drzraf](https://github.com/drzraf)) +* Search for block within all ascendants instead of parent only ([#698](https://github.com/twigjs/twig.js/pull/698) by [@drzraf](https://github.com/drzraf)) + +Minor improvements: +* Fix autoescape for empty includes ([#687](https://github.com/twigjs/twig.js/pull/687) by [@tgabi333](https://github.com/tgabi333)) +* Fix filters with empty string input ([#690](https://github.com/twigjs/twig.js/pull/690) by [@tbence94](https://github.com/tbence94)) + +Version 1.14.0, released 2019-11-13 +----------------------------------- +Major improvements: +* Add [Babel](https://babeljs.io) to the webpack build + +Minor improvements: +* Add `apply` tag ([#656](https://github.com/twigjs/twig.js/pull/656) by [@maxhelias](https://github.com/maxhelias)) +* Add `spaceless` filter ([#655](https://github.com/twigjs/twig.js/pull/655) by [@maxhelias](https://github.com/maxhelias)) +* Add `deprecated` tag ([#675](https://github.com/twigjs/twig.js/pull/675) by [@josephineb](https://github.com/josephineb)) +* Fix `starts with` and `ends with` expressions ([#661](https://github.com/twigjs/twig.js/pull/661) by [@ilkkave](https://github.com/ilkkave)) +* Add `package.json` license field to fix npm warning ([#672](https://github.com/twigjs/twig.js/pull/672) by [@WietseWind](https://github.com/WietseWind)) +* Update `strict_variables` option to match Twig's strict messages ([#674](https://github.com/twigjs/twig.js/pull/674) by [@toptalo](https://github.com/toptalo)) +* Fix `??` operator when used with arrays to return the array rather than its length ([#653](https://github.com/twigjs/twig.js/pull/653) by [@diegorales](https://github.com/diegomorales)) + +Version 1.13.3, released 2019-05-03 +----------------------------------- +Minor improvements: +* Allow project development on Windows ([#611](https://github.com/twigjs/twig.js/pull/611)) +* Add possibility to define namespace without slash at the end of the path ([#609](https://github.com/twigjs/twig.js/pull/609)) +* Update `verbatim` tag ([#584](https://github.com/twigjs/twig.js/pull/584)) + +Version 1.13.2, released 2019-01-22 +----------------------------------- +Minor improvements: +* fix for not autoescaping includes having a parent ([#606](https://github.com/twigjs/twig.js/pull/606)) + +Version 1.13.1, released 2019-01-19 +----------------------------------- +Minor improvements: +* Fix for not autoescaping includes ([#604](https://github.com/twigjs/twig.js/pull/604)) + +Version 1.13.0, released 2019-01-09 +----------------------------------- + +Major improvements: +* Unminified sources in the npm package ([#598](https://github.com/twigjs/twig.js/pull/598)) + +Minor improvements: +* Multiple namespace performance improvement ([#580](https://github.com/twigjs/twig.js/pull/580)) +* `|url_encode` can now extend parameters ([#588](https://github.com/twigjs/twig.js/pull/588)) +* Fix `.startsWith` and `.endsWith` with `.indexOf` for IE ([#587](https://github.com/twigjs/twig.js/pull/587)) +* Autoescaping improvement ([#577](https://github.com/twigjs/twig.js/pull/577)) +* Support null-coalescing operator `??` ([#575](https://github.com/twigjs/twig.js/pull/575)) +* Add `verbatim` tag ([#574](https://github.com/twigjs/twig.js/pull/574)) +* Fix bug in `for` loop ([#573](https://github.com/twigjs/twig.js/pull/573)) +* Fix twig `{% if(x) %}` and `{% elseif(x) %}` blocks parsing error ([#570](https://github.com/twigjs/twig.js/pull/570)) + +Version 1.12.0, released 2018-06-11 +----------------------------------- + +Major improvements: +* Fix array declaration on multiple lines (#546) +* Fix extend when add is null (#559) + +Minor improvements: +* Improve namespaces support (#556) +* Allow express to render async (#558) + +Version 1.11.1, released 2018-05-22 +----------------------------------- + +Major improvements: +* Upgrade to Webpack 4 (#542) +* Fix embed blocks logic (#537) + +Minor improvements: +* Improve detection of when a block is in a loop (#541) +* Add possibility to set default value for a macro parameter (#544) + +Version 1.11.0, released 2018-04-10 +----------------------------------- + +Major improvements: +* Add support for 'with' tag (#497) +* Add date support for json_encode filter (#515) +* Fix 'embed' tag options (#534) +* Performance improvements when including templates (#492) + +Minor improvements: +* Fix incorrect 'and' and 'or' behaviour when comparing variables (#481) +* Remove 'trim' filter autoescape (#488) +* Fix empty output from loop with async call (#538) +* Add allowable tags to strip tags filter (#524) + + +Version 1.10.5, released 2017-05-24 +----------------------------------- + +Minor improvements: +* Template id is now returned as part of the error when an exception is thrown (#464) +* Test result no longer dependent on the name of the test file (#465) +* Fix unexpected 'const' (#471) + +Version 1.10.4, released 2017-03-02 +----------------------------------- + +Minor improvements: +* Fixed missing changelog updates + +Version 1.10.3, released 2017-03-02 +----------------------------------- + +Major improvements: +* Async rendering and filters (#457) +* From aliases (#438) +* Bitwise operators (#443) +* Fix object method context (#455) + +Minor improvements: +* Readme updates (#454) +* 'not' unary can be more widely used (#444) +* Fix `importFile` relative path handling (#449) + +Version 0.10.3, released 2016-12-09 +----------------------------------- +Minor improvements: +* Spaceless tag no longer escapes Static values (#435) +* Inline includes now load (#433) +* Errors during async fs loading use error callback (#431) + +Version 0.10.2, released 2016-11-23 +----------------------------------- +Minor improvements: +* Support 'same as' (#429) +* Fix windows colon colon namespace (#430) + +Version 0.10.1, released 2016-11-18 +----------------------------------- + +Minor improvements: +* Fixed missing changelog updates +* Fixed incorrect versions in source +* Rethrow errors when option to do so is set (#422) + +Version 0.10.0, released 2016-10-28 +----------------------------------- +Bower is no longer supported + +Major improvements: +* Updated to locutus which replaces phpjs +* elseif now accepts truthy values (#370) +* Use PHP style falsy matching (#383) +* Fix 'not' after binary expressions (#385) +* Use current context when parsing an include (#395) +* Correct handling of 'ignore missing' in embed and include (#424) + +Minor improvements: +* Documentation updates +* Refreshed dependencies + +Version 0.9.5, released 2016-05-14 +----------------------------------- + +Minor improvements: +* Templates that are included via "extends" now populate the parent template context + +Version 0.9.4, released 2016-05-13 +----------------------------------- +Parentheses parsing has undergone quite a large refactoring, but nothing should have explicitly broken. + +Major improvements: +* Subexpressions are now supported and parsed differently from function parameters + +Version 0.9.3, released 2016-05-12 +----------------------------------- +Fix missing changelog updates + +Version 0.9.2, released 2016-05-12 +----------------------------------- +Minor improvements: +* Empty strings can now be passed to the date filter +* Twig.expression.resolve keeps the correct context for `this` + +Version 0.9.1, released 2016-05-10 +----------------------------------- +Fixed changelog versioning + +Version 0.9.0, released 2016-05-10 +----------------------------------- +Theoretically no breaking changes, but lots of things have changed so it is possible something has slipped through. + +Dependencies have been updated. You should run `npm install` to update these. + +Major improvements: +* Webpack is now used for builds +* phpjs is now a dependency and replaces our reimplementation of PHP functions (#343) +* Arrays are now cast to booleans unless accessing their contents +* in/not in operator precedence changed (#344) +* Expressions can now be keys (#350) +* The extended ternary operator is now supported (#354) +* Expressions can now appear after period accessor (#356) +* The slice shorthand is now supported (#362) + +Minor improvements: +* Twig.exports.renderFile now returns a string rather than a String (#348) +* The value of context is now cloned when setting a variable to context (#345) +* Negative numbers are now correctly parsed (#353) +* The // operator now works correctly (#353) + + +Version 0.8.9, released 2016-03-18 +----------------------------- +Dependencies have been updated to current versions. You should run `npm install` to update these. (#313) + +Major improvements: +* Twig's `source` function is now supported (#309) +* It is possible to add additional parsers using Twig.Templates.registerParser() (currently available: twig, source). If you are using a custom loader, please investigate src/twig.loader.fs.js how to call the requested parser. (#309) +* `undefined` and `null` values now supported in the `in` operator (#311) +* Namespaces can now be defined using the '@' symbol (#328) + +Minor improvements: +* Undefined object properties now have the value of `undefined` rather than `null` (#311) +* Improved browser tests (#325, #310) +* IE8 fix (#324) +* Path resolution has been refactored to its own module (#323) + +Version 0.8.8, released 2016-02-13 +---------------------------------- +Major improvements: +* Support for [block shortcuts](http://twig.sensiolabs.org/doc/tags/extends.html#block-shortcuts): `{% block title page_title|title %}` (#304) +* Define custom template loaders, by registering them via `Twig.Templates.registerLoader()` (#301) + +Minor improvements: +* Some mocha tests didn't work in browsers (#281) +* Fix Twig.renderFile (#303) + +[All issues of this milestone](https://github.com/justjohn/twig.js/issues?q=milestone%3A0.8.8) + +Version 0.8.7, released 2016-01-20 +---------------------------------- +Major improvements: +* The `autoescape` option now supports all strategies which are supported by the `escape` filter (#299) + +Minor improvements: +* The `date` filter now recognises unix timestamps as input, when they are passed as string (#296) +* The `default` filter now allows to be called without parameters (it will return an empty string in that case) (#295) +* Normalize provided template paths (this generated problems when using nodejs under Windows) (#252, #300) + +Version 0.8.6, released 2016-01-05 +---------------------------------- +Major improvements: +* The `escape` filter now supports the strategy parameter: `{{ var|escape('css') }}` with the following available strategies: html (default), js, css, url, html_attr. (#289) + +Minor improvements: +* The filter `url_encode` now also encodes apostrophe (as in Twig.php) (#288) +* Minor bugfixes (#290, #291) + +Version 0.8.5, released 2015-12-24 +---------------------------------- +From 0.8.5 on, a summary of changes between each version will be included in the CHANGELOG.md file. + +There were some changes to the [Contribution guidelines](https://github.com/justjohn/twig.js/wiki/Contributing): please commit only changes to source files, the files `twig.js` and `twig.min.js` will be rebuilt when a new version gets released. Therefore you need to run `make` after cloning resp. pulling (if you want to use the development version). + +Major improvements: +* Implement `min` and `max` functions (#164) +* Support for the whitespace control modifier: `{{- -}}` (#266) +* `sort` filter: try to cast values to match type (numeric values to number, string otherwise) (#278) +* Support for twig namespaces (#195, #251) +* Support for expressions as object keys: `{% set foo = { (1 + 1): 'bar' } %}` (#284) + +Minor improvements: +* Allow integer 0 as key in objects: `{ 0: "value" }` (#186) +* `json_encode` filter: always return objects in order of keys, also ignore the internal key `_keys` for nested objects (#279) +* `date` filter: update to current strtotime() function from phpjs: now support ISO8601 dates as input on Mozilla Firefox. (#276) +* Validate template IDs only when caching is enabled (#233, #259) +* Support xmlhttp.status==0 when using cordova (#240) +* Improved sub template file loading (#264) +* Ignore quotes between `{% raw %}` and `{% endraw %}` (#286) diff --git a/LICENSE b/LICENSE index e4e6ec0b..f2911cd3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2013, John Roepke +Copyright (c) 2011-2015, John Roepke and the Twig.js Contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -6,4 +6,5 @@ Redistribution and use in source and binary forms, with or without modification, Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Makefile b/Makefile deleted file mode 100644 index 11159e5f..00000000 --- a/Makefile +++ /dev/null @@ -1,59 +0,0 @@ -SRC = src/twig.header.js src/twig.core.js src/twig.fills.js src/twig.lib.js src/twig.logic.js src/twig.expression.js src/twig.expression.operator.js src/twig.filters.js src/twig.functions.js src/twig.tests.js src/twig.exports.js src/twig.compiler.js src/twig.module.js - -TESTS = test/*.js -TESTSEXT = test-ext/*.js -REPORTER = spec - -UGLIFY = ./node_modules/uglify-js/bin/uglifyjs -DOCCO = ./node_modules/docco/bin/docco -MOCHA = ./node_modules/mocha/bin/mocha - -all: twig.js twig.min.js - -test: - @NODE_ENV=test $(MOCHA) \ - --require should \ - --reporter $(REPORTER) \ - --timeout 100 \ - --growl \ - $(TESTS) - -twig.php: - ./test-ext/checkout.sh - -testphp: twig.php - @NODE_ENV=test $(MOCHA) \ - --require should \ - --reporter $(REPORTER) \ - --timeout 100 \ - --growl \ - $(TESTSEXT) - -twig.js: $(SRC) - cat $^ > $@ - cp $@ demos/node_express/public/vendor/ - cp $@ demos/twitter_backbone/vendor/ - -twig.min.js: twig.js - $(UGLIFY) \ - --source-map $@.map \ - --comments "@license" \ - $< > $@ - -docs: test-docs annotated-docs - -test-docs: - @NODE_ENV=test $(MOCHA) \ - --require should \ - --reporter markdown \ - --timeout 100 \ - --growl \ - $(TESTS) > docs/tests.md - -annotated-docs: $(SRC) - $(DOCCO) twig.js - -clean: - rm -f twig.min.js twig.js - -.PHONY: test text-ext docs test-docs clean diff --git a/README.md b/README.md index 3c88b66a..12b3fffb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,16 @@ -[![Build Status](https://secure.travis-ci.org/justjohn/twig.js.png)](http://travis-ci.org/#!/justjohn/twig.js) -[![NPM version](https://badge.fury.io/js/twig.png)](http://badge.fury.io/js/twig) +[![Known Vulnerabilities](https://snyk.io/test/github/twigjs/twig.js/badge.svg)](https://snyk.io/test/github/twigjs/twig.js) +[![Build Status](https://secure.travis-ci.org/twigjs/twig.js.svg)](http://travis-ci.org/twigjs/twig.js) +[![NPM version](https://badge.fury.io/js/twig.svg)](http://badge.fury.io/js/twig) +[![Gitter](https://badges.gitter.im/twigjs/twig.js.svg)](https://gitter.im/twigjs/twig.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) # About + + Twig.js is a pure JavaScript implementation of the Twig PHP templating language -() +() The goal is to provide a library that is compatible with both browsers and server side JavaScript environments such as node.js. @@ -12,28 +18,30 @@ Twig.js is currently a work in progress and supports a limited subset of the Twi ### Docs -Documentation is available in the [twig.js wiki](https://github.com/justjohn/twig.js/wiki) on Github. +Documentation is available in the [twig.js wiki](https://github.com/twigjs/twig.js/wiki) on Github. ### Feature Support -For a list of supported tags/filters/functions/tests see the [Implementation Notes](https://github.com/justjohn/twig.js/wiki/Implementation-Notes) page on the wiki. +For a list of supported tags/filters/functions/tests see the [Implementation Notes](https://github.com/twigjs/twig.js/wiki/Implementation-Notes) page on the wiki. -### Contributing +# Install -If you have a change you want to make to twig.js, feel free to fork this repository and submit a pull request on Github. The source files are located in src/*.js. twig.js is built by running `make` +Download the latest twig.js release from github: https://github.com/twigjs/twig.js/releases or via NPM: -For more details on getting setup, see the [contributing page](https://github.com/justjohn/twig.js/wiki/Contributing) on the wiki. +```bash +npm install twig --save +``` -# Browser Usage +# Bower -Twig.js can be installed as a bower package with: +A bower package is available from [philsbury](https://github.com/philsbury/twigjs-bower). Please direct any Bower support issues to that repo. - bower install twig.js +## Browser Usage Include twig.js or twig.min.js in your page, then: ```js -var template = twig({ +var template = Twig.twig({ data: 'The {{ baked_good }} is a lie.' }); @@ -43,20 +51,37 @@ console.log( // outputs: "The cupcake is a lie." ``` -# Node Usage +## Webpack + +A loader is available from [zimmo.be](https://github.com/zimmo-be/twig-loader). -Twig.js can be installed with NPM +## Node Usage (npm) - npm install twig +Tested on node >=6.0. You can use twig in your app with - var Twig = require('twig'), // Twig module - twig = Twig.twig; // Render function +```js +var Twig = require('twig'), // Twig module + twig = Twig.twig; // Render function +``` + +### Usage without Express + +If you don't want to use Express, you can render a template with the following method: -Twig is compatable with express 2 and 3. You can create an express app using the twig.js templating language by setting the view engine to twig. +```js +import Twig from 'twig'; +Twig.renderFile('./path/to/someFile.twig', {foo:'bar'}, (err, html) => { + html; // compiled string +}); +``` -## app.js +### Usage with Express + +Twig is compatible with express 2 and 3. You can create an express app using the twig.js templating language by setting the view engine to twig. + +### app.js **Express 3** @@ -67,6 +92,7 @@ var Twig = require("twig"), // This section is optional and used to configure twig. app.set("twig options", { + allowAsync: true, // Allow asynchronous compiling strict_variables: false }); @@ -85,17 +111,32 @@ app.listen(9999); Message of the moment: {{ message }} ``` -An [Express 2 Example](https://github.com/justjohn/twig.js/wiki/Express-2) is available on the wiki. +An [Express 2 Example](https://github.com/twigjs/twig.js/wiki/Express-2) is available on the wiki. + +# Alternatives + +- [Twing](https://github.com/ericmorand/twing) + +# Contributing + +If you have a change you want to make to twig.js, feel free to fork this repository and submit a pull request on Github. The source files are located in `src/*.js`. + +twig.js is built by running `npm run build` + +For more details on getting setup, see the [contributing page](https://github.com/twigjs/twig.js/wiki/Contributing) on the wiki. + +## Environment Requirements +When developing on Windows, the repository must be checked out **without** automatic conversion of LF to CRLF. Failure to do so will cause tests that would otherwise pass on Linux or Mac to fail instead. -# Tests +## Tests -The twig.js tests are written in [Mocha][mocha] and can be invoked with `make test`. +The twig.js tests are written in [Mocha][mocha] and can be invoked with `npm test`. -# License +## License Twig.js is available under a [BSD 2-Clause License][bsd-2], see the LICENSE file for more information. -# Acknowledgments +## Acknowledgments See the LICENSES.md file for copies of the referenced licenses. @@ -116,5 +157,5 @@ See the LICENSES.md file for copies of the referenced licenses. [bsd-3]: http://www.opensource.org/licenses/BSD-3-Clause [cc-by-sa-2.5]: http://creativecommons.org/licenses/by-sa/2.5/ "Creative Commons Attribution-ShareAlike 2.5 License" -[mocha]: http://visionmedia.github.com/mocha/ +[mocha]: http://mochajs.org/ [qunit]: http://docs.jquery.com/QUnit diff --git a/bin/twigjs b/bin/twigjs index cfacb852..291f5f71 100755 --- a/bin/twigjs +++ b/bin/twigjs @@ -24,7 +24,7 @@ while (args.length > 0) { return; case "--output": case "-o": - options.output = PATHS.strip_slash(args.shift()); + options.output = PATHS.stripSlash(args.shift()); break; case "--pattern": case "-p": diff --git a/bower.json b/bower.json deleted file mode 100644 index 11393a3b..00000000 --- a/bower.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "twig.js", - "version": "0.7.2", - "main": "twig.min.js", - "ignore": [ - "Makefile", - "src", - "demos", - "bin", - "lib", - "**/.*", - "node_modules", - "bower_components", - "test", - "test-ext" - ], - "homepage": "https://github.com/justjohn/twig.js", - "authors": [ - "John Roepke " - ], - "description": "JS implementation of the Twig templating language.", - "keywords": [ - "template", - "engine", - "twig" - ], - "license": "BSD 2-Clause" -} diff --git a/demos/node_express/.gitignore b/demos/node_express/.gitignore new file mode 100644 index 00000000..483a9c42 --- /dev/null +++ b/demos/node_express/.gitignore @@ -0,0 +1 @@ +package-lock.json \ No newline at end of file diff --git a/demos/node_express/app.js b/demos/node_express/app.js index a5ea3929..30934cd4 100644 --- a/demos/node_express/app.js +++ b/demos/node_express/app.js @@ -1,143 +1,139 @@ -var twig = require("../../twig") - , _ = require("underscore")._ - , markdown = require("markdown") - , express = require('express') - , app = express.createServer(); +const twig = require('twig'); +const {_} = require('underscore'); + const markdown = require('markdown'); +const express = require('express'); +const bodyParser = require('body-parser'); +const app = express(); // Generate some function error_json(id, message) { - return { - error: true - , id: id - , message: message - , json: true - } + return { + error: true, + id, + message, + json: true + }; } function update_note(body) { - var title = body.title; - var text = body.text; - var id = body.id; + const {title} = body; + const {text} = body; + let {id} = body; if (title) { - if (id == "") { + if (id == '') { // Get new ID and increment ID counter id = id_ctr; id_ctr++; } notes[id] = { - title: title - , text: text - , id: id + title, + text, + id }; - console.log("Adding/Updating note"); + console.log('Adding/Updating note'); console.log(notes[id]); } - } // Some test data to pre-populate the notebook with var id_ctr = 4; var notes = { 1: { - title: "Note" - , text: "These could be your **notes**.\n\nBut you would have to turn this demo program into something beautiful." - , id: 1 - } - , 2: { - title: "Templates" - , text: "Templates are a way of enhancing content with markup. Or really anything that requires the merging of data and display." - , id: 2 - } - , 3: { - title: "Tasks" - , text: "* Wake Up\n* Drive to Work\n* Work\n* Drive Home\n* Sleep" - , id: 3 + title: 'Note', + text: 'These could be your **notes**.\n\nBut you would have to turn this demo program into something beautiful.', + id: 1 + }, + 2: { + title: 'Templates', + text: 'Templates are a way of enhancing content with markup. Or really anything that requires the merging of data and display.', + id: 2 + }, + 3: { + title: 'Tasks', + text: '* Wake Up\n* Drive to Work\n* Work\n* Drive Home\n* Sleep', + id: 3 } }; -app.configure(function () { - app.use(express.static(__dirname + '/public')); - app.use(express.bodyParser()); - app.set('views', __dirname + '/public/views'); - app.set('view engine', 'twig'); - // We don't need express to use a parent "page" layout - // Twig.js has support for this using the {% extends parent %} tag - app.set("view options", { layout: false }); -}); - -app.register('twig', twig); +app.use(express.static(__dirname + '/public')); +app.use(bodyParser()); +app.set('views', __dirname + '/public/views'); +app.set('view engine', 'twig'); +// We don't need express to use a parent "page" layout +// Twig.js has support for this using the {% extends parent %} tag +app.set('view options', {layout: false}); // Routing for the notebook -app.get('/', function(req, res){ - res.render('pages/index', { - message : "Hello World" - }); +app.get('/', (req, res) => { + res.render('pages/index', { + message: 'Hello World' + }); }); -app.get('/add', function(req, res) { - res.render('pages/note_form', {}); +app.get('/add', (req, res) => { + res.render('pages/note_form', {}); }); -app.get('/edit/:id', function(req, res) { - var id = parseInt(req.params.id); - var note = notes[id]; +app.get('/edit/:id', (req, res) => { + const id = parseInt(req.params.id); + const note = notes[id]; - res.render('pages/note_form', note); + res.render('pages/note_form', note); }); -app.all('/notes', function(req, res) { - update_note(req.body); +app.all('/notes', (req, res) => { + update_note(req.body); - res.render('pages/notes', { - notes : notes - }); + res.render('pages/notes', { + notes + }); }); -app.all('/notes/:id', function(req, res) { - update_note(req.body); +app.all('/notes/:id', (req, res) => { + update_note(req.body); - var id = parseInt(req.params.id); - var note = notes[id]; + const id = parseInt(req.params.id); + const note = notes[id]; - if (note) { - note.markdown = markdown.markdown.toHTML( note.text ); + if (note) { + note.markdown = markdown.markdown.toHTML(note.text); res.render('pages/note', note); - } else { - res.render('pages/note_404'); - } + } else { + res.render('pages/note_404'); + } }); // RESTFUL endpoint for notes -app.get('/api/notes', function(req, res) { - res.json({ - notes : notes - , json: true - }); +app.get('/api/notes', (req, res) => { + res.json({ + notes, + json: true + }); }); -app.get('/api/notes/:id', function(req, res) { - var id = parseInt(req.params.id); - var note = notes[id]; +app.get('/api/notes/:id', (req, res) => { + const id = parseInt(req.params.id); + const note = notes[id]; - if (note) { - note.markdown = markdown.markdown.toHTML( note.text ); + if (note) { + note.markdown = markdown.markdown.toHTML(note.text); res.json(_.extend({ json: true }, note)); - } else { - res.json(error_json(41, "Unable to find note with id " + id)) - } + } else { + res.json(error_json(41, 'Unable to find note with id ' + id)); + } }); -var port = process.env.PORT || 9999, - host = process.env.IP || "0.0.0.0"; +const port = process.env.PORT || 9999; +const host = process.env.IP || '0.0.0.0'; app.listen(port, host); -console.log("Express Twig.js Demo is running on " + host + ":" + port); +console.log('Express Twig.js Demo is running on ' + host + ':' + port); diff --git a/demos/node_express/package.json b/demos/node_express/package.json index 9870a334..532b9510 100644 --- a/demos/node_express/package.json +++ b/demos/node_express/package.json @@ -1,25 +1,34 @@ { "author": "John Roepke (http://johnroepke.com/)", "name": "twig-demo-node", + "private": true, "description": "Demo using node and twig.", "version": "0.3.0", - "homepage": "https://github.com/justjohn/twig.js", + "homepage": "https://github.com/twigjs/twig.js", "licenses": [ - { "type": "BSD-2-Clause", - "url": "https://raw.github.com/justjohn/twig.js/master/LICENSE" } + { + "type": "BSD-2-Clause", + "url": "https://raw.github.com/twigjs/twig.js/master/LICENSE" + } ], + "scripts": { + "install": "cd ../.. && npm it", + "start": "node app.js" + }, "repository": { "type": "git", - "url": "git://github.com/justjohn/twig.js.git" + "url": "git://github.com/twigjs/twig.js.git" }, "main": "app.js", "engines": { "node": "*" }, "dependencies": { - "express": "2.5.x", - "markdown": "0.3.x", - "underscore": "1.3.x" + "body-parser": "^1.19.0", + "express": "4.17.x", + "markdown": "0.5.x", + "underscore": "1.9.x", + "twig": "file:../.." }, "devDependencies": {} } diff --git a/demos/node_express/public/js/app.js b/demos/node_express/public/js/app.js index 25dab8dd..81ae7d2a 100644 --- a/demos/node_express/public/js/app.js +++ b/demos/node_express/public/js/app.js @@ -1,123 +1,126 @@ // Notebook client code Twig.cache = true; -(function(window,undefined){ - var base = "/views/" - api_base = "/api"; - - crossroads.addRoute('/', function(){ +(function (window, undefined) { + const base = '/views/'; + api_base = '/api'; + + crossroads.addRoute('/', () => { // Load notes page - var template = twig({ref: "index"}) - , output = template.render({json:true}); - - $("#noteApp").html(output); + const template = twig({ref: 'index'}); + const output = template.render({json: true}); + + $('#noteApp').html(output); }); - - crossroads.addRoute('/notes', function(){ + + crossroads.addRoute('/notes', () => { // Load notes page - var template = twig({ref: "notes"}) - , url = api_base + "/notes"; - - $.getJSON(url, function(data) { - var output = template.render(data); - $("#noteApp").html(output); + const template = twig({ref: 'notes'}); + const url = api_base + '/notes'; + + $.getJSON(url, data => { + const output = template.render(data); + $('#noteApp').html(output); }); }); - - crossroads.addRoute('/notes/{id}', function(id) { + + crossroads.addRoute('/notes/{id}', id => { // Load notes page - var template = twig({ref: "note"}) - , error_template = twig({ref: "404"}) - , url = api_base + "/notes/" + id; - - $.getJSON(url, function(data) { - var output; + const template = twig({ref: 'note'}); + const error_template = twig({ref: '404'}); + const url = api_base + '/notes/' + id; + + $.getJSON(url, data => { + let output; if (data.error) { output = error_template.render(data); } else { output = template.render(data); } - $("#noteApp").html(output); + + $('#noteApp').html(output); }); }); - - crossroads.addRoute('/add', function(){ + + crossroads.addRoute('/add', () => { // Load notes page - var template = twig({ref: "form"}) - , output = template.render({json:true}); - - $("#noteApp").html(output); + const template = twig({ref: 'form'}); + const output = template.render({json: true}); + + $('#noteApp').html(output); }); - - crossroads.addRoute('/edit/{id}', function(id) { + + crossroads.addRoute('/edit/{id}', id => { // Load notes page - var template = twig({ref: "form"}) - , error_template = twig({ref: "404"}) - , url = api_base + "/notes/" + id; - - $.getJSON(url, function(data) { - var output; + const template = twig({ref: 'form'}); + const error_template = twig({ref: '404'}); + const url = api_base + '/notes/' + id; + + $.getJSON(url, data => { + let output; if (data.error) { output = error_template.render(data); } else { output = template.render(data); } - $("#noteApp").html(output); + + $('#noteApp').html(output); }); }); - + // Preload templates - (function() { - var loaded = 0 - , count = 5 - , inc_loaded = function() { - loaded++; - if (loaded == count) { - // Flag as loaded, signal any waiting events - } + (function () { + let loaded = 0; + const count = 5; + const inc_loaded = function () { + loaded++; + if (loaded == count) { + // Flag as loaded, signal any waiting events } - , pages = { - "note": "pages/note.twig" - , "notes": "pages/notes.twig" - , "index": "pages/index.twig" - , "form": "pages/note_form.twig" - , "404": "pages/note_404.twig" - }; - + }; + + const pages = { + note: 'pages/note.twig', + notes: 'pages/notes.twig', + index: 'pages/index.twig', + form: 'pages/note_form.twig', + 404: 'pages/note_404.twig' + }; + for (id in pages) { if (pages.hasOwnProperty(id)) { twig({ - id: id - , href: base + pages[id] - , load: function() { + id, + href: base + pages[id], + load() { inc_loaded(); } - }); - }; + }); + } } })(); - - var History = window.History; + + const {History} = window; // Don't bind AJAX events without history support - if ( !History.enabled ) { + if (!History.enabled) { return false; } - $(function() { + $(() => { // Bind to StateChange Event - History.Adapter.bind(window,'statechange',function(){ // Note: We are using statechange instead of popstate - var State = History.getState() - , hash = State.hash; - + History.Adapter.bind(window, 'statechange', () => { // Note: We are using statechange instead of popstate + const State = History.getState(); + const {hash} = State; + console.log(hash); // Trigger router crossroads.parse(hash); }); // Bind to links - $("a.ajax_link").live("click", function(event) { + $('a.ajax_link').live('click', function (event) { event.preventDefault(); - var href = $(this).attr("href"); + const href = $(this).attr('href'); History.pushState(null, null, href); }); }); diff --git a/demos/node_express/public/vendor/jquery.history.js b/demos/node_express/public/vendor/jquery.history.js index 8d4edcd2..dedbc20f 100644 --- a/demos/node_express/public/vendor/jquery.history.js +++ b/demos/node_express/public/vendor/jquery.history.js @@ -1 +1,480 @@ -window.JSON||(window.JSON={}),function(){function f(a){return a<10?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c")&&c[0]);return a>4?a:!1}();return a},m.isInternetExplorer=function(){var a=m.isInternetExplorer.cached=typeof m.isInternetExplorer.cached!="undefined"?m.isInternetExplorer.cached:Boolean(m.getInternetExplorerMajorVersion());return a},m.emulated={pushState:!Boolean(a.history&&a.history.pushState&&a.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)),hashChange:Boolean(!("onhashchange"in a||"onhashchange"in d)||m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8)},m.enabled=!m.emulated.pushState,m.bugs={setHash:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),safariPoll:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),ieDoubleCheck:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<7)},m.isEmptyObject=function(a){for(var b in a)return!1;return!0},m.cloneObject=function(a){var b,c;return a?(b=k.stringify(a),c=k.parse(b)):c={},c},m.getRootUrl=function(){var a=d.location.protocol+"//"+(d.location.hostname||d.location.host);if(d.location.port||!1)a+=":"+d.location.port;return a+="/",a},m.getBaseHref=function(){var a=d.getElementsByTagName("base"),b=null,c="";return a.length===1&&(b=a[0],c=b.href.replace(/[^\/]+$/,"")),c=c.replace(/\/+$/,""),c&&(c+="/"),c},m.getBaseUrl=function(){var a=m.getBaseHref()||m.getBasePageUrl()||m.getRootUrl();return a},m.getPageUrl=function(){var a=m.getState(!1,!1),b=(a||{}).url||d.location.href,c;return c=b.replace(/\/+$/,"").replace(/[^\/]+$/,function(a,b,c){return/\./.test(a)?a:a+"/"}),c},m.getBasePageUrl=function(){var a=d.location.href.replace(/[#\?].*/,"").replace(/[^\/]+$/,function(a,b,c){return/[^\/]$/.test(a)?"":a}).replace(/\/+$/,"")+"/";return a},m.getFullUrl=function(a,b){var c=a,d=a.substring(0,1);return b=typeof b=="undefined"?!0:b,/[a-z]+\:\/\//.test(a)||(d==="/"?c=m.getRootUrl()+a.replace(/^\/+/,""):d==="#"?c=m.getPageUrl().replace(/#.*/,"")+a:d==="?"?c=m.getPageUrl().replace(/[\?#].*/,"")+a:b?c=m.getBaseUrl()+a.replace(/^(\.\/)+/,""):c=m.getBasePageUrl()+a.replace(/^(\.\/)+/,"")),c.replace(/\#$/,"")},m.getShortUrl=function(a){var b=a,c=m.getBaseUrl(),d=m.getRootUrl();return m.emulated.pushState&&(b=b.replace(c,"")),b=b.replace(d,"/"),m.isTraditionalAnchor(b)&&(b="./"+b),b=b.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),b},m.store={},m.idToState=m.idToState||{},m.stateToId=m.stateToId||{},m.urlToId=m.urlToId||{},m.storedStates=m.storedStates||[],m.savedStates=m.savedStates||[],m.normalizeStore=function(){m.store.idToState=m.store.idToState||{},m.store.urlToId=m.store.urlToId||{},m.store.stateToId=m.store.stateToId||{}},m.getState=function(a,b){typeof a=="undefined"&&(a=!0),typeof b=="undefined"&&(b=!0);var c=m.getLastSavedState();return!c&&b&&(c=m.createStateObject()),a&&(c=m.cloneObject(c),c.url=c.cleanUrl||c.url),c},m.getIdByState=function(a){var b=m.extractId(a.url),c;if(!b){c=m.getStateString(a);if(typeof m.stateToId[c]!="undefined")b=m.stateToId[c];else if(typeof m.store.stateToId[c]!="undefined")b=m.store.stateToId[c];else{for(;;){b=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof m.idToState[b]=="undefined"&&typeof m.store.idToState[b]=="undefined")break}m.stateToId[c]=b,m.idToState[b]=a}}return b},m.normalizeState=function(a){var b,c;if(!a||typeof a!="object")a={};if(typeof a.normalized!="undefined")return a;if(!a.data||typeof a.data!="object")a.data={};b={},b.normalized=!0,b.title=a.title||"",b.url=m.getFullUrl(m.unescapeString(a.url||d.location.href)),b.hash=m.getShortUrl(b.url),b.data=m.cloneObject(a.data),b.id=m.getIdByState(b),b.cleanUrl=b.url.replace(/\??\&_suid.*/,""),b.url=b.cleanUrl,c=!m.isEmptyObject(b.data);if(b.title||c)b.hash=m.getShortUrl(b.url).replace(/\??\&_suid.*/,""),/\?/.test(b.hash)||(b.hash+="?"),b.hash+="&_suid="+b.id;return b.hashedUrl=m.getFullUrl(b.hash),(m.emulated.pushState||m.bugs.safariPoll)&&m.hasUrlDuplicate(b)&&(b.url=b.hashedUrl),b},m.createStateObject=function(a,b,c){var d={data:a,title:b,url:c};return d=m.normalizeState(d),d},m.getStateById=function(a){a=String(a);var c=m.idToState[a]||m.store.idToState[a]||b;return c},m.getStateString=function(a){var b,c,d;return b=m.normalizeState(a),c={data:b.data,title:a.title,url:a.url},d=k.stringify(c),d},m.getStateId=function(a){var b,c;return b=m.normalizeState(a),c=b.id,c},m.getHashByState=function(a){var b,c;return b=m.normalizeState(a),c=b.hash,c},m.extractId=function(a){var b,c,d;return c=/(.*)\&_suid=([0-9]+)$/.exec(a),d=c?c[1]||a:a,b=c?String(c[2]||""):"",b||!1},m.isTraditionalAnchor=function(a){var b=!/[\/\?\.]/.test(a);return b},m.extractState=function(a,b){var c=null,d,e;return b=b||!1,d=m.extractId(a),d&&(c=m.getStateById(d)),c||(e=m.getFullUrl(a),d=m.getIdByUrl(e)||!1,d&&(c=m.getStateById(d)),!c&&b&&!m.isTraditionalAnchor(a)&&(c=m.createStateObject(null,null,e))),c},m.getIdByUrl=function(a){var c=m.urlToId[a]||m.store.urlToId[a]||b;return c},m.getLastSavedState=function(){return m.savedStates[m.savedStates.length-1]||b},m.getLastStoredState=function(){return m.storedStates[m.storedStates.length-1]||b},m.hasUrlDuplicate=function(a){var b=!1,c;return c=m.extractState(a.url),b=c&&c.id!==a.id,b},m.storeState=function(a){return m.urlToId[a.url]=a.id,m.storedStates.push(m.cloneObject(a)),a},m.isLastSavedState=function(a){var b=!1,c,d,e;return m.savedStates.length&&(c=a.id,d=m.getLastSavedState(),e=d.id,b=c===e),b},m.saveState=function(a){return m.isLastSavedState(a)?!1:(m.savedStates.push(m.cloneObject(a)),!0)},m.getStateByIndex=function(a){var b=null;return typeof a=="undefined"?b=m.savedStates[m.savedStates.length-1]:a<0?b=m.savedStates[m.savedStates.length+a]:b=m.savedStates[a],b},m.getHash=function(){var a=m.unescapeHash(d.location.hash);return a},m.unescapeString=function(b){var c=b,d;for(;;){d=a.unescape(c);if(d===c)break;c=d}return c},m.unescapeHash=function(a){var b=m.normalizeHash(a);return b=m.unescapeString(b),b},m.normalizeHash=function(a){var b=a.replace(/[^#]*#/,"").replace(/#.*/,"");return b},m.setHash=function(a,b){var c,e,f;return b!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.setHash,args:arguments,queue:b}),!1):(c=m.escapeHash(a),m.busy(!0),e=m.extractState(a,!0),e&&!m.emulated.pushState?m.pushState(e.data,e.title,e.url,!1):d.location.hash!==c&&(m.bugs.setHash?(f=m.getPageUrl(),m.pushState(null,null,f+"#"+c,!1)):d.location.hash=c),m)},m.escapeHash=function(b){var c=m.normalizeHash(b);return c=a.escape(c),m.bugs.hashEscape||(c=c.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),c},m.getHashByUrl=function(a){var b=String(a).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return b=m.unescapeHash(b),b},m.setTitle=function(a){var b=a.title,c;b||(c=m.getStateByIndex(0),c&&c.url===a.url&&(b=c.title||m.options.initialTitle));try{d.getElementsByTagName("title")[0].innerHTML=b.replace("<","<").replace(">",">").replace(" & "," & ")}catch(e){}return d.title=b,m},m.queues=[],m.busy=function(a){typeof a!="undefined"?m.busy.flag=a:typeof m.busy.flag=="undefined"&&(m.busy.flag=!1);if(!m.busy.flag){h(m.busy.timeout);var b=function(){var a,c,d;if(m.busy.flag)return;for(a=m.queues.length-1;a>=0;--a){c=m.queues[a];if(c.length===0)continue;d=c.shift(),m.fireQueueItem(d),m.busy.timeout=g(b,m.options.busyDelay)}};m.busy.timeout=g(b,m.options.busyDelay)}return m.busy.flag},m.busy.flag=!1,m.fireQueueItem=function(a){return a.callback.apply(a.scope||m,a.args||[])},m.pushQueue=function(a){return m.queues[a.queue||0]=m.queues[a.queue||0]||[],m.queues[a.queue||0].push(a),m},m.queue=function(a,b){return typeof a=="function"&&(a={callback:a}),typeof b!="undefined"&&(a.queue=b),m.busy()?m.pushQueue(a):m.fireQueueItem(a),m},m.clearQueue=function(){return m.busy.flag=!1,m.queues=[],m},m.stateChanged=!1,m.doubleChecker=!1,m.doubleCheckComplete=function(){return m.stateChanged=!0,m.doubleCheckClear(),m},m.doubleCheckClear=function(){return m.doubleChecker&&(h(m.doubleChecker),m.doubleChecker=!1),m},m.doubleCheck=function(a){return m.stateChanged=!1,m.doubleCheckClear(),m.bugs.ieDoubleCheck&&(m.doubleChecker=g(function(){return m.doubleCheckClear(),m.stateChanged||a(),!0},m.options.doubleCheckInterval)),m},m.safariStatePoll=function(){var b=m.extractState(d.location.href),c;if(!m.isLastSavedState(b))c=b;else return;return c||(c=m.createStateObject()),m.Adapter.trigger(a,"popstate"),m},m.back=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.back,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.back(!1)}),n.go(-1),!0)},m.forward=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.forward,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.forward(!1)}),n.go(1),!0)},m.go=function(a,b){var c;if(a>0)for(c=1;c<=a;++c)m.forward(b);else{if(!(a<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(c=-1;c>=a;--c)m.back(b)}return m};if(m.emulated.pushState){var o=function(){};m.pushState=m.pushState||o,m.replaceState=m.replaceState||o}else m.onPopState=function(b,c){var e=!1,f=!1,g,h;return m.doubleCheckComplete(),g=m.getHash(),g?(h=m.extractState(g||d.location.href,!0),h?m.replaceState(h.data,h.title,h.url,!1):(m.Adapter.trigger(a,"anchorchange"),m.busy(!1)),m.expectedStateId=!1,!1):(e=m.Adapter.extractEventData("state",b,c)||!1,e?f=m.getStateById(e):m.expectedStateId?f=m.getStateById(m.expectedStateId):f=m.extractState(d.location.href),f||(f=m.createStateObject(null,null,d.location.href)),m.expectedStateId=!1,m.isLastSavedState(f)?(m.busy(!1),!1):(m.storeState(f),m.saveState(f),m.setTitle(f),m.Adapter.trigger(a,"statechange"),m.busy(!1),!0))},m.Adapter.bind(a,"popstate",m.onPopState),m.pushState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.pushState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.pushState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0},m.replaceState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.replaceState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.replaceState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0};if(f){try{m.store=k.parse(f.getItem("History.store"))||{}}catch(p){m.store={}}m.normalizeStore()}else m.store={},m.normalizeStore();m.Adapter.bind(a,"beforeunload",m.clearAllIntervals),m.Adapter.bind(a,"unload",m.clearAllIntervals),m.saveState(m.storeState(m.extractState(d.location.href,!0))),f&&(m.onUnload=function(){var a,b;try{a=k.parse(f.getItem("History.store"))||{}}catch(c){a={}}a.idToState=a.idToState||{},a.urlToId=a.urlToId||{},a.stateToId=a.stateToId||{};for(b in m.idToState){if(!m.idToState.hasOwnProperty(b))continue;a.idToState[b]=m.idToState[b]}for(b in m.urlToId){if(!m.urlToId.hasOwnProperty(b))continue;a.urlToId[b]=m.urlToId[b]}for(b in m.stateToId){if(!m.stateToId.hasOwnProperty(b))continue;a.stateToId[b]=m.stateToId[b]}m.store=a,m.normalizeStore(),f.setItem("History.store",k.stringify(a))},m.intervalList.push(i(m.onUnload,m.options.storeInterval)),m.Adapter.bind(a,"beforeunload",m.onUnload),m.Adapter.bind(a,"unload",m.onUnload));if(!m.emulated.pushState){m.bugs.safariPoll&&m.intervalList.push(i(m.safariStatePoll,m.options.safariPollInterval));if(e.vendor==="Apple Computer, Inc."||(e.appCodeName||"")==="Mozilla")m.Adapter.bind(a,"hashchange",function(){m.Adapter.trigger(a,"popstate")}),m.getHash()&&m.Adapter.onDomLoad(function(){m.Adapter.trigger(a,"hashchange")})}},m.init()}(window) \ No newline at end of file +window.JSON || (window.JSON = {}), (function () { + function f(a) { + return a < 10 ? '0' + a : a; + } + + function quote(a) { + return escapable.lastIndex = 0, escapable.test(a) ? '"' + a.replace(escapable, a => { + const b = meta[a]; return typeof b === 'string' ? b : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + a + '"'; + } + + function str(a, b) { + let c; let d; let e; let f; const g = gap; let h; let i = b[a]; i && typeof i === 'object' && typeof i.toJSON === 'function' && (i = i.toJSON(a)), typeof rep === 'function' && (i = rep.call(b, a, i)); switch (typeof i) { + case 'string': return quote(i); case 'number': return isFinite(i) ? String(i) : 'null'; case 'boolean': case 'null': return String(i); case 'object': if (!i) { + return 'null'; + } + + gap += indent, h = []; if (Object.prototype.toString.apply(i) === '[object Array]') { + f = i.length; for (c = 0; c < f; c += 1) { + h[c] = str(c, i) || 'null'; + } + + return e = h.length === 0 ? '[]' : (gap ? '[\n' + gap + h.join(',\n' + gap) + '\n' + g + ']' : '[' + h.join(',') + ']'), gap = g, e; + } + + if (rep && typeof rep === 'object') { + f = rep.length; for (c = 0; c < f; c += 1) { + d = rep[c], typeof d === 'string' && (e = str(d, i), e && h.push(quote(d) + (gap ? ': ' : ':') + e)); + } + } else { + for (d in i) { + Object.hasOwnProperty.call(i, d) && (e = str(d, i), e && h.push(quote(d) + (gap ? ': ' : ':') + e)); + } + } + + return e = h.length === 0 ? '{}' : (gap ? '{\n' + gap + h.join(',\n' + gap) + '\n' + g + '}' : '{' + h.join(',') + '}'), gap = g, e; + } + } + + 'use strict', typeof Date.prototype.toJSON !== 'function' && (Date.prototype.toJSON = function (a) { + return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; + }, String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (a) { + return this.valueOf(); + }); const {JSON} = window; const cx = /[\u0000\u00AD\u0600-\u0604\u070F\u17B4\u17B5\u200C-\u200F\u2028-\u202F\u2060-\u206f\ufeff\ufff0-\uffff]/g; var escapable = /[\\\"\u0000-\u001F\u007F-\u009F\u00AD\u0600-\u0604\u070F\u17B4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; let gap; let indent; var meta = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"': '\\"', '\\': '\\\\'}; let + rep; typeof JSON.stringify !== 'function' && (JSON.stringify = function (a, b, c) { + let d; gap = '', indent = ''; if (typeof c === 'number') { + for (d = 0; d < c; d += 1) { + indent += ' '; + } + } else { + typeof c === 'string' && (indent = c); + } + + rep = b; if (!b || typeof b === 'function' || typeof b === 'object' && typeof b.length === 'number') { + return str('', {'': a}); + } + + throw new Error('JSON.stringify'); + }), typeof JSON.parse !== 'function' && (JSON.parse = function (text, reviver) { + function walk(a, b) { + let c; let d; const e = a[b]; if (e && typeof e === 'object') { + for (c in e) { + Object.hasOwnProperty.call(e, c) && (d = walk(e, c), d !== undefined ? e[c] = d : delete e[c]); + } + } + + return reviver.call(a, b, e); + } + + let j; text = String(text), cx.lastIndex = 0, cx.test(text) && (text = text.replace(cx, a => { + return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + })); if (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + return j = eval('(' + text + ')'), typeof reviver === 'function' ? walk({'': j}, '') : j; + } + + throw new SyntaxError('JSON.parse'); + }); +})(), (function (a, b) { + 'use strict'; const c = a.History = a.History || {}; const d = a.jQuery; if (typeof c.Adapter !== 'undefined') { + throw new TypeError('History.js Adapter has already been loaded...'); + } + + c.Adapter = {bind(a, b, c) { + d(a).bind(b, c); + }, trigger(a, b, c) { + d(a).trigger(b, c); + }, extractEventData(a, c, d) { + const e = c && c.originalEvent && c.originalEvent[a] || d && d[a] || b; return e; + }, onDomLoad(a) { + d(a); + }}, typeof c.init !== 'undefined' && c.init(); +})(window), (function (a, b) { + 'use strict'; const c = a.document; var d = a.setTimeout || d; var e = a.clearTimeout || e; var f = a.setInterval || f; const g = a.History = a.History || {}; if (typeof g.initHtml4 !== 'undefined') { + throw new TypeError('History.js HTML4 Support has already been loaded...'); + } + + g.initHtml4 = function () { + if (typeof g.initHtml4.initialized !== 'undefined') { + return !1; + } + + g.initHtml4.initialized = !0, g.enabled = !0, g.savedHashes = [], g.isLastHash = function (a) { + const b = g.getHashByIndex(); let c; return c = a === b, c; + }, g.saveHash = function (a) { + return g.isLastHash(a) ? !1 : (g.savedHashes.push(a), !0); + }, g.getHashByIndex = function (a) { + let b = null; return typeof a === 'undefined' ? b = g.savedHashes[g.savedHashes.length - 1] : (a < 0 ? b = g.savedHashes[g.savedHashes.length + a] : b = g.savedHashes[a]), b; + }, g.discardedHashes = {}, g.discardedStates = {}, g.discardState = function (a, b, c) { + const d = g.getHashByState(a); let e; return e = {discardedState: a, backState: c, forwardState: b}, g.discardedStates[d] = e, !0; + }, g.discardHash = function (a, b, c) { + const d = {discardedHash: a, backState: c, forwardState: b}; return g.discardedHashes[a] = d, !0; + }, g.discardedState = function (a) { + const b = g.getHashByState(a); let c; return c = g.discardedStates[b] || !1, c; + }, g.discardedHash = function (a) { + const b = g.discardedHashes[a] || !1; return b; + }, g.recycleState = function (a) { + const b = g.getHashByState(a); return g.discardedState(a) && delete g.discardedStates[b], !0; + }, g.emulated.hashChange && (g.hashChangeInit = function () { + g.checkerFunction = null; let b = ''; let d; let e; let h; let i; return g.isInternetExplorer() ? (d = 'historyjs-iframe', e = c.createElement('iframe'), e.setAttribute('id', d), e.style.display = 'none', c.body.appendChild(e), e.contentWindow.document.open(), e.contentWindow.document.close(), h = '', i = !1, g.checkerFunction = function () { + if (i) { + return !1; + } + + i = !0; const c = g.getHash() || ''; let d = g.unescapeHash(e.contentWindow.document.location.hash) || ''; return c !== b ? (b = c, d !== c && (h = d = c, e.contentWindow.document.open(), e.contentWindow.document.close(), e.contentWindow.document.location.hash = g.escapeHash(c)), g.Adapter.trigger(a, 'hashchange')) : d !== h && (h = d, g.setHash(d, !1)), i = !1, !0; + }) : g.checkerFunction = function () { + const c = g.getHash(); return c !== b && (b = c, g.Adapter.trigger(a, 'hashchange')), !0; + }, g.intervalList.push(f(g.checkerFunction, g.options.hashChangeInterval)), !0; + }, g.Adapter.onDomLoad(g.hashChangeInit)), g.emulated.pushState && (g.onHashChange = function (b) { + const d = b && b.newURL || c.location.href; const e = g.getHashByUrl(d); let f = null; let h = null; const i = null; let j; return g.isLastHash(e) ? (g.busy(!1), !1) : (g.doubleCheckComplete(), g.saveHash(e), e && g.isTraditionalAnchor(e) ? (g.Adapter.trigger(a, 'anchorchange'), g.busy(!1), !1) : (f = g.extractState(g.getFullUrl(e || c.location.href, !1), !0), g.isLastSavedState(f) ? (g.busy(!1), !1) : (h = g.getHashByState(f), j = g.discardedState(f), j ? (g.getHashByIndex(-2) === g.getHashByState(j.forwardState) ? g.back(!1) : g.forward(!1), !1) : (g.pushState(f.data, f.title, f.url, !1), !0)))); + }, g.Adapter.bind(a, 'hashchange', g.onHashChange), g.pushState = function (b, d, e, f) { + if (g.getHashByUrl(e)) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } + + if (f !== !1 && g.busy()) { + return g.pushQueue({scope: g, callback: g.pushState, args: arguments, queue: f}), !1; + } + + g.busy(!0); const h = g.createStateObject(b, d, e); const i = g.getHashByState(h); const j = g.getState(!1); const k = g.getHashByState(j); const l = g.getHash(); return g.storeState(h), g.expectedStateId = h.id, g.recycleState(h), g.setTitle(h), i === k ? (g.busy(!1), !1) : (i !== l && i !== g.getShortUrl(c.location.href) ? (g.setHash(i, !1), !1) : (g.saveState(h), g.Adapter.trigger(a, 'statechange'), g.busy(!1), !0)); + }, g.replaceState = function (a, b, c, d) { + if (g.getHashByUrl(c)) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } + + if (d !== !1 && g.busy()) { + return g.pushQueue({scope: g, callback: g.replaceState, args: arguments, queue: d}), !1; + } + + g.busy(!0); const e = g.createStateObject(a, b, c); const f = g.getState(!1); const h = g.getStateByIndex(-2); return g.discardState(f, e, h), g.pushState(e.data, e.title, e.url, !1), !0; + }), g.emulated.pushState && g.getHash() && !g.emulated.hashChange && g.Adapter.onDomLoad(() => { + g.Adapter.trigger(a, 'hashchange'); + }); + }, typeof g.init !== 'undefined' && g.init(); +})(window), (function (a, b) { + 'use strict'; const c = a.console || b; const d = a.document; const e = a.navigator; const f = a.sessionStorage || !1; const g = a.setTimeout; const h = a.clearTimeout; const i = a.setInterval; const j = a.clearInterval; const k = a.JSON; const l = a.alert; const m = a.History = a.History || {}; const n = a.history; k.stringify = k.stringify || k.encode, k.parse = k.parse || k.decode; if (typeof m.init !== 'undefined') { + throw new TypeError('History.js Core has already been loaded...'); + } + + m.init = function () { + return typeof m.Adapter === 'undefined' ? !1 : (typeof m.initCore !== 'undefined' && m.initCore(), typeof m.initHtml4 !== 'undefined' && m.initHtml4(), !0); + }, m.initCore = function () { + if (typeof m.initCore.initialized !== 'undefined') { + return !1; + } + + m.initCore.initialized = !0, m.options = m.options || {}, m.options.hashChangeInterval = m.options.hashChangeInterval || 100, m.options.safariPollInterval = m.options.safariPollInterval || 500, m.options.doubleCheckInterval = m.options.doubleCheckInterval || 500, m.options.storeInterval = m.options.storeInterval || 1e3, m.options.busyDelay = m.options.busyDelay || 250, m.options.debug = m.options.debug || !1, m.options.initialTitle = m.options.initialTitle || d.title, m.intervalList = [], m.clearAllIntervals = function () { + let a; const b = m.intervalList; if (typeof b !== 'undefined' && b !== null) { + for (a = 0; a < b.length; a++) { + j(b[a]); + } + + m.intervalList = null; + } + }, m.debug = function () { + (m.options.debug || !1) && m.log.apply(m, arguments); + }, m.log = function () { + const a = typeof c !== 'undefined' && typeof c.log !== 'undefined' && typeof c.log.apply !== 'undefined'; const b = d.querySelector('#log'); let e; let f; let g; let h; let i; a ? (h = Array.prototype.slice.call(arguments), e = h.shift(), typeof c.debug !== 'undefined' ? c.debug.apply(c, [e, h]) : c.log.apply(c, [e, h])) : e = '\n' + arguments[0] + '\n'; for (f = 1, g = arguments.length; f < g; ++f) { + i = arguments[f]; if (typeof i === 'object' && typeof k !== 'undefined') { + try { + i = k.stringify(i); + } catch (error) {} + } + + e += '\n' + i + '\n'; + } + + return b ? (b.value += e + '\n-----\n', b.scrollTop = b.scrollHeight - b.clientHeight) : a || l(e), !0; + }, m.getInternetExplorerMajorVersion = function () { + const a = m.getInternetExplorerMajorVersion.cached = typeof m.getInternetExplorerMajorVersion.cached !== 'undefined' ? m.getInternetExplorerMajorVersion.cached : (function () { + let a = 3; const b = d.createElement('div'); const c = b.querySelectorAll('i'); while ((b.innerHTML = '') && c[0]) { + + } + + return a > 4 ? a : !1; + })(); return a; + }, m.isInternetExplorer = function () { + const a = m.isInternetExplorer.cached = typeof m.isInternetExplorer.cached !== 'undefined' ? m.isInternetExplorer.cached : Boolean(m.getInternetExplorerMajorVersion()); return a; + }, m.emulated = {pushState: !(a.history && a.history.pushState && a.history.replaceState && !/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent) && !/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)), hashChange: Boolean(!('onhashchange' in a || 'onhashchange' in d) || m.isInternetExplorer() && m.getInternetExplorerMajorVersion() < 8)}, m.enabled = !m.emulated.pushState, m.bugs = {setHash: Boolean(!m.emulated.pushState && e.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)), safariPoll: Boolean(!m.emulated.pushState && e.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)), ieDoubleCheck: Boolean(m.isInternetExplorer() && m.getInternetExplorerMajorVersion() < 8), hashEscape: Boolean(m.isInternetExplorer() && m.getInternetExplorerMajorVersion() < 7)}, m.isEmptyObject = function (a) { + for (const b in a) { + return !1; + } + + return !0; + }, m.cloneObject = function (a) { + let b; let c; return a ? (b = k.stringify(a), c = k.parse(b)) : c = {}, c; + }, m.getRootUrl = function () { + let a = d.location.protocol + '//' + (d.location.hostname || d.location.host); if (d.location.port || !1) { + a += ':' + d.location.port; + } + + return a += '/', a; + }, m.getBaseHref = function () { + const a = d.querySelectorAll('base'); let b = null; let c = ''; return a.length === 1 && (b = a[0], c = b.href.replace(/[^\/]+$/, '')), c = c.replace(/\/+$/, ''), c && (c += '/'), c; + }, m.getBaseUrl = function () { + const a = m.getBaseHref() || m.getBasePageUrl() || m.getRootUrl(); return a; + }, m.getPageUrl = function () { + const a = m.getState(!1, !1); const b = (a || {}).url || d.location.href; let c; return c = b.replace(/\/+$/, '').replace(/[^\/]+$/, (a, b, c) => { + return /\./.test(a) ? a : a + '/'; + }), c; + }, m.getBasePageUrl = function () { + const a = d.location.href.replace(/[#\?].*/, '').replace(/[^\/]+$/, (a, b, c) => { + return /[^\/]$/.test(a) ? '' : a; + }).replace(/\/+$/, '') + '/'; return a; + }, m.getFullUrl = function (a, b) { + let c = a; const d = a.slice(0, 1); return b = typeof b === 'undefined' ? !0 : b, /[a-z]+\:\/\//.test(a) || (d === '/' ? c = m.getRootUrl() + a.replace(/^\/+/, '') : d === '#' ? c = m.getPageUrl().replace(/#.*/, '') + a : d === '?' ? c = m.getPageUrl().replace(/[\?#].*/, '') + a : (b ? c = m.getBaseUrl() + a.replace(/^(\.\/)+/, '') : c = m.getBasePageUrl() + a.replace(/^(\.\/)+/, ''))), c.replace(/\#$/, ''); + }, m.getShortUrl = function (a) { + let b = a; const c = m.getBaseUrl(); const d = m.getRootUrl(); return m.emulated.pushState && (b = b.replace(c, '')), b = b.replace(d, '/'), m.isTraditionalAnchor(b) && (b = './' + b), b = b.replace(/^(\.\/)+/g, './').replace(/\#$/, ''), b; + }, m.store = {}, m.idToState = m.idToState || {}, m.stateToId = m.stateToId || {}, m.urlToId = m.urlToId || {}, m.storedStates = m.storedStates || [], m.savedStates = m.savedStates || [], m.normalizeStore = function () { + m.store.idToState = m.store.idToState || {}, m.store.urlToId = m.store.urlToId || {}, m.store.stateToId = m.store.stateToId || {}; + }, m.getState = function (a, b) { + typeof a === 'undefined' && (a = !0), typeof b === 'undefined' && (b = !0); let c = m.getLastSavedState(); return !c && b && (c = m.createStateObject()), a && (c = m.cloneObject(c), c.url = c.cleanUrl || c.url), c; + }, m.getIdByState = function (a) { + let b = m.extractId(a.url); let c; if (!b) { + c = m.getStateString(a); if (typeof m.stateToId[c] !== 'undefined') { + b = m.stateToId[c]; + } else if (typeof m.store.stateToId[c] !== 'undefined') { + b = m.store.stateToId[c]; + } else { + for (;;) { + b = (new Date()).getTime() + String(Math.random()).replace(/\D/g, ''); if (typeof m.idToState[b] === 'undefined' && typeof m.store.idToState[b] === 'undefined') { + break; + } + } + + m.stateToId[c] = b, m.idToState[b] = a; + } + } + + return b; + }, m.normalizeState = function (a) { + let b; let c; if (!a || typeof a !== 'object') { + a = {}; + } + + if (typeof a.normalized !== 'undefined') { + return a; + } + + if (!a.data || typeof a.data !== 'object') { + a.data = {}; + } + + b = {}, b.normalized = !0, b.title = a.title || '', b.url = m.getFullUrl(m.unescapeString(a.url || d.location.href)), b.hash = m.getShortUrl(b.url), b.data = m.cloneObject(a.data), b.id = m.getIdByState(b), b.cleanUrl = b.url.replace(/\??\&_suid.*/, ''), b.url = b.cleanUrl, c = !m.isEmptyObject(b.data); if (b.title || c) { + b.hash = m.getShortUrl(b.url).replace(/\??\&_suid.*/, ''), /\?/.test(b.hash) || (b.hash += '?'), b.hash += '&_suid=' + b.id; + } + + return b.hashedUrl = m.getFullUrl(b.hash), (m.emulated.pushState || m.bugs.safariPoll) && m.hasUrlDuplicate(b) && (b.url = b.hashedUrl), b; + }, m.createStateObject = function (a, b, c) { + let d = {data: a, title: b, url: c}; return d = m.normalizeState(d), d; + }, m.getStateById = function (a) { + a = String(a); const c = m.idToState[a] || m.store.idToState[a] || b; return c; + }, m.getStateString = function (a) { + let b; let c; let d; return b = m.normalizeState(a), c = {data: b.data, title: a.title, url: a.url}, d = k.stringify(c), d; + }, m.getStateId = function (a) { + let b; let c; return b = m.normalizeState(a), c = b.id, c; + }, m.getHashByState = function (a) { + let b; let c; return b = m.normalizeState(a), c = b.hash, c; + }, m.extractId = function (a) { + let b; let c; let d; return c = /(.*)\&_suid=([0-9]+)$/.exec(a), d = c ? c[1] || a : a, b = c ? String(c[2] || '') : '', b || !1; + }, m.isTraditionalAnchor = function (a) { + const b = !/[\/\?\.]/.test(a); return b; + }, m.extractState = function (a, b) { + let c = null; let d; let e; return b = b || !1, d = m.extractId(a), d && (c = m.getStateById(d)), c || (e = m.getFullUrl(a), d = m.getIdByUrl(e) || !1, d && (c = m.getStateById(d)), !c && b && !m.isTraditionalAnchor(a) && (c = m.createStateObject(null, null, e))), c; + }, m.getIdByUrl = function (a) { + const c = m.urlToId[a] || m.store.urlToId[a] || b; return c; + }, m.getLastSavedState = function () { + return m.savedStates[m.savedStates.length - 1] || b; + }, m.getLastStoredState = function () { + return m.storedStates[m.storedStates.length - 1] || b; + }, m.hasUrlDuplicate = function (a) { + let b = !1; let c; return c = m.extractState(a.url), b = c && c.id !== a.id, b; + }, m.storeState = function (a) { + return m.urlToId[a.url] = a.id, m.storedStates.push(m.cloneObject(a)), a; + }, m.isLastSavedState = function (a) { + let b = !1; let c; let d; let e; return m.savedStates.length && (c = a.id, d = m.getLastSavedState(), e = d.id, b = c === e), b; + }, m.saveState = function (a) { + return m.isLastSavedState(a) ? !1 : (m.savedStates.push(m.cloneObject(a)), !0); + }, m.getStateByIndex = function (a) { + let b = null; return typeof a === 'undefined' ? b = m.savedStates[m.savedStates.length - 1] : (a < 0 ? b = m.savedStates[m.savedStates.length + a] : b = m.savedStates[a]), b; + }, m.getHash = function () { + const a = m.unescapeHash(d.location.hash); return a; + }, m.unescapeString = function (b) { + let c = b; let d; for (;;) { + d = a.unescape(c); if (d === c) { + break; + } + + c = d; + } + + return c; + }, m.unescapeHash = function (a) { + let b = m.normalizeHash(a); return b = m.unescapeString(b), b; + }, m.normalizeHash = function (a) { + const b = a.replace(/[^#]*#/, '').replace(/#.*/, ''); return b; + }, m.setHash = function (a, b) { + let c; let e; let f; return b !== !1 && m.busy() ? (m.pushQueue({scope: m, callback: m.setHash, args: arguments, queue: b}), !1) : (c = m.escapeHash(a), m.busy(!0), e = m.extractState(a, !0), e && !m.emulated.pushState ? m.pushState(e.data, e.title, e.url, !1) : d.location.hash !== c && (m.bugs.setHash ? (f = m.getPageUrl(), m.pushState(null, null, f + '#' + c, !1)) : d.location.hash = c), m); + }, m.escapeHash = function (b) { + let c = m.normalizeHash(b); return c = a.escape(c), m.bugs.hashEscape || (c = c.replace(/\%21/g, '!').replace(/\%26/g, '&').replace(/\%3D/g, '=').replace(/\%3F/g, '?')), c; + }, m.getHashByUrl = function (a) { + let b = String(a).replace(/([^#]*)#?([^#]*)#?(.*)/, '$2'); return b = m.unescapeHash(b), b; + }, m.setTitle = function (a) { + let b = a.title; let c; b || (c = m.getStateByIndex(0), c && c.url === a.url && (b = c.title || m.options.initialTitle)); try { + d.querySelectorAll('title')[0].innerHTML = b.replace('<', '<').replace('>', '>').replace(' & ', ' & '); + } catch (error) {} + + return d.title = b, m; + }, m.queues = [], m.busy = function (a) { + typeof a !== 'undefined' ? m.busy.flag = a : typeof m.busy.flag === 'undefined' && (m.busy.flag = !1); if (!m.busy.flag) { + h(m.busy.timeout); var b = function () { + let a; let c; let d; if (m.busy.flag) { + return; + } + + for (a = m.queues.length - 1; a >= 0; --a) { + c = m.queues[a]; if (c.length === 0) { + continue; + } + + d = c.shift(), m.fireQueueItem(d), m.busy.timeout = g(b, m.options.busyDelay); + } + }; + + m.busy.timeout = g(b, m.options.busyDelay); + } + + return m.busy.flag; + }, m.busy.flag = !1, m.fireQueueItem = function (a) { + return a.callback.apply(a.scope || m, a.args || []); + }, m.pushQueue = function (a) { + return m.queues[a.queue || 0] = m.queues[a.queue || 0] || [], m.queues[a.queue || 0].push(a), m; + }, m.queue = function (a, b) { + return typeof a === 'function' && (a = {callback: a}), typeof b !== 'undefined' && (a.queue = b), m.busy() ? m.pushQueue(a) : m.fireQueueItem(a), m; + }, m.clearQueue = function () { + return m.busy.flag = !1, m.queues = [], m; + }, m.stateChanged = !1, m.doubleChecker = !1, m.doubleCheckComplete = function () { + return m.stateChanged = !0, m.doubleCheckClear(), m; + }, m.doubleCheckClear = function () { + return m.doubleChecker && (h(m.doubleChecker), m.doubleChecker = !1), m; + }, m.doubleCheck = function (a) { + return m.stateChanged = !1, m.doubleCheckClear(), m.bugs.ieDoubleCheck && (m.doubleChecker = g(() => { + return m.doubleCheckClear(), m.stateChanged || a(), !0; + }, m.options.doubleCheckInterval)), m; + }, m.safariStatePoll = function () { + const b = m.extractState(d.location.href); let c; if (!m.isLastSavedState(b)) { + c = b; + } else { + return; + } + + return c || (c = m.createStateObject()), m.Adapter.trigger(a, 'popstate'), m; + }, m.back = function (a) { + return a !== !1 && m.busy() ? (m.pushQueue({scope: m, callback: m.back, args: arguments, queue: a}), !1) : (m.busy(!0), m.doubleCheck(() => { + m.back(!1); + }), n.go(-1), !0); + }, m.forward = function (a) { + return a !== !1 && m.busy() ? (m.pushQueue({scope: m, callback: m.forward, args: arguments, queue: a}), !1) : (m.busy(!0), m.doubleCheck(() => { + m.forward(!1); + }), n.go(1), !0); + }, m.go = function (a, b) { + let c; if (a > 0) { + for (c = 1; c <= a; ++c) { + m.forward(b); + } + } else { + if (!(a < 0)) { + throw new Error('History.go: History.go requires a positive or negative integer passed.'); + } + + for (c = -1; c >= a; --c) { + m.back(b); + } + } + + return m; + }; + + if (m.emulated.pushState) { + const o = function () {}; m.pushState = m.pushState || o, m.replaceState = m.replaceState || o; + } else { + m.onPopState = function (b, c) { + let e = !1; let f = !1; let g; let h; return m.doubleCheckComplete(), g = m.getHash(), g ? (h = m.extractState(g || d.location.href, !0), h ? m.replaceState(h.data, h.title, h.url, !1) : (m.Adapter.trigger(a, 'anchorchange'), m.busy(!1)), m.expectedStateId = !1, !1) : (e = m.Adapter.extractEventData('state', b, c) || !1, e ? f = m.getStateById(e) : (m.expectedStateId ? f = m.getStateById(m.expectedStateId) : f = m.extractState(d.location.href)), f || (f = m.createStateObject(null, null, d.location.href)), m.expectedStateId = !1, m.isLastSavedState(f) ? (m.busy(!1), !1) : (m.storeState(f), m.saveState(f), m.setTitle(f), m.Adapter.trigger(a, 'statechange'), m.busy(!1), !0)); + }, m.Adapter.bind(a, 'popstate', m.onPopState), m.pushState = function (b, c, d, e) { + if (m.getHashByUrl(d) && m.emulated.pushState) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } + + if (e !== !1 && m.busy()) { + return m.pushQueue({scope: m, callback: m.pushState, args: arguments, queue: e}), !1; + } + + m.busy(!0); const f = m.createStateObject(b, c, d); return m.isLastSavedState(f) ? m.busy(!1) : (m.storeState(f), m.expectedStateId = f.id, n.pushState(f.id, f.title, f.url), m.Adapter.trigger(a, 'popstate')), !0; + }, m.replaceState = function (b, c, d, e) { + if (m.getHashByUrl(d) && m.emulated.pushState) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } + + if (e !== !1 && m.busy()) { + return m.pushQueue({scope: m, callback: m.replaceState, args: arguments, queue: e}), !1; + } + + m.busy(!0); const f = m.createStateObject(b, c, d); return m.isLastSavedState(f) ? m.busy(!1) : (m.storeState(f), m.expectedStateId = f.id, n.replaceState(f.id, f.title, f.url), m.Adapter.trigger(a, 'popstate')), !0; + }; + } + + if (f) { + try { + m.store = k.parse(f.getItem('History.store')) || {}; + } catch (error) { + m.store = {}; + } + + m.normalizeStore(); + } else { + m.store = {}, m.normalizeStore(); + } + + m.Adapter.bind(a, 'beforeunload', m.clearAllIntervals), m.Adapter.bind(a, 'unload', m.clearAllIntervals), m.saveState(m.storeState(m.extractState(d.location.href, !0))), f && (m.onUnload = function () { + let a; let b; try { + a = k.parse(f.getItem('History.store')) || {}; + } catch (error) { + a = {}; + } + + a.idToState = a.idToState || {}, a.urlToId = a.urlToId || {}, a.stateToId = a.stateToId || {}; for (b in m.idToState) { + if (!m.idToState.hasOwnProperty(b)) { + continue; + } + + a.idToState[b] = m.idToState[b]; + } + + for (b in m.urlToId) { + if (!m.urlToId.hasOwnProperty(b)) { + continue; + } + + a.urlToId[b] = m.urlToId[b]; + } + + for (b in m.stateToId) { + if (!m.stateToId.hasOwnProperty(b)) { + continue; + } + + a.stateToId[b] = m.stateToId[b]; + } + + m.store = a, m.normalizeStore(), f.setItem('History.store', k.stringify(a)); + }, m.intervalList.push(i(m.onUnload, m.options.storeInterval)), m.Adapter.bind(a, 'beforeunload', m.onUnload), m.Adapter.bind(a, 'unload', m.onUnload)); if (!m.emulated.pushState) { + m.bugs.safariPoll && m.intervalList.push(i(m.safariStatePoll, m.options.safariPollInterval)); if (e.vendor === 'Apple Computer, Inc.' || (e.appCodeName || '') === 'Mozilla') { + m.Adapter.bind(a, 'hashchange', () => { + m.Adapter.trigger(a, 'popstate'); + }), m.getHash() && m.Adapter.onDomLoad(() => { + m.Adapter.trigger(a, 'hashchange'); + }); + } + } + }, m.init(); +})(window); diff --git a/demos/node_express/public/vendor/twig.js b/demos/node_express/public/vendor/twig.js index 2eebc11d..00914b98 100644 --- a/demos/node_express/public/vendor/twig.js +++ b/demos/node_express/public/vendor/twig.js @@ -1,19 +1,18 @@ /** - * Twig.js 0.7.2 + * Twig.js 0.8.9 * - * @copyright 2011-2013 John Roepke + * @copyright 2011-2015 John Roepke and the Twig.js Contributors * @license Available under the BSD 2-Clause License * @link https://github.com/justjohn/twig.js */ var Twig = (function (Twig) { - Twig.VERSION = "0.7.2"; + Twig.VERSION = "0.8.9"; return Twig; })(Twig || {}); // Twig.js -// Copyright (c) 2011-2013 John Roepke // Available under the BSD 2-Clause License // https://github.com/justjohn/twig.js @@ -132,6 +131,18 @@ var Twig = (function (Twig) { // 8. return undefined }; + Twig.merge = function(target, source, onlyChanged) { + Twig.forEach(Object.keys(source), function (key) { + if (onlyChanged && !(key in target)) { + return; + } + + target[key] = source[key] + }); + + return target; + }; + /** * Exception thrown by twig.js. */ @@ -155,18 +166,35 @@ var Twig = (function (Twig) { */ Twig.log = { trace: function() {if (Twig.trace && console) {console.log(Array.prototype.slice.call(arguments));}}, - debug: function() {if (Twig.debug && console) {console.log(Array.prototype.slice.call(arguments));}}, + debug: function() {if (Twig.debug && console) {console.log(Array.prototype.slice.call(arguments));}} }; - if (typeof console !== "undefined" && - typeof console.log !== "undefined") { - Twig.log.error = function() { - console.log.apply(console, arguments); + + if (typeof console !== "undefined") { + if (typeof console.error !== "undefined") { + Twig.log.error = function() { + console.error.apply(console, arguments); + } + } else if (typeof console.log !== "undefined") { + Twig.log.error = function() { + console.log.apply(console, arguments); + } } } else { Twig.log.error = function(){}; } + /** + * Wrapper for child context objects in Twig. + * + * @param {Object} context Values to initialize the context with. + */ + Twig.ChildContext = function(context) { + var ChildContext = function ChildContext() {}; + ChildContext.prototype = context; + return new ChildContext(); + }; + /** * Container for methods related to handling high level template tokens * (for example: {{ expression }}, {% logic %}, {# comment #}, raw data) @@ -177,10 +205,16 @@ var Twig = (function (Twig) { * Token types. */ Twig.token.type = { - output: 'output', - logic: 'logic', - comment: 'comment', - raw: 'raw' + output: 'output', + logic: 'logic', + comment: 'comment', + raw: 'raw', + output_whitespace_pre: 'output_whitespace_pre', + output_whitespace_post: 'output_whitespace_post', + output_whitespace_both: 'output_whitespace_both', + logic_whitespace_pre: 'logic_whitespace_pre', + logic_whitespace_post: 'logic_whitespace_post', + logic_whitespace_both: 'logic_whitespace_both' }; /** @@ -192,6 +226,44 @@ var Twig = (function (Twig) { open: '{% raw %}', close: '{% endraw %}' }, + { + type: Twig.token.type.raw, + open: '{% verbatim %}', + close: '{% endverbatim %}' + }, + // *Whitespace type tokens* + // + // These typically take the form `{{- expression -}}` or `{{- expression }}` or `{{ expression -}}`. + { + type: Twig.token.type.output_whitespace_pre, + open: '{{-', + close: '}}' + }, + { + type: Twig.token.type.output_whitespace_post, + open: '{{', + close: '-}}' + }, + { + type: Twig.token.type.output_whitespace_both, + open: '{{-', + close: '-}}' + }, + { + type: Twig.token.type.logic_whitespace_pre, + open: '{%-', + close: '%}' + }, + { + type: Twig.token.type.logic_whitespace_post, + open: '{%', + close: '-%}' + }, + { + type: Twig.token.type.logic_whitespace_both, + open: '{%-', + close: '-%}' + }, // *Output type tokens* // // These typically take the form `{{ expression }}`. @@ -228,25 +300,69 @@ var Twig = (function (Twig) { Twig.token.findStart = function (template) { var output = { position: null, + close_position: null, def: null }, i, token_template, - first_key_position; + first_key_position, + close_key_position; for (i=0;i= 0) { + //This token matches the template + if (token_template.open.length !== token_template.close.length) { + //This token has mismatched closing and opening tags + if (close_key_position < 0) { + //This token's closing tag does not match the template + continue; + } + } + } // Does this token occur before any other types? if (first_key_position >= 0 && (output.position === null || first_key_position < output.position)) { output.position = first_key_position; output.def = token_template; + output.close_position = close_key_position; + } else if (first_key_position >= 0 && output.position !== null && first_key_position === output.position) { + /*This token exactly matches another token, + greedily match to check if this token has a greater specificity*/ + if (token_template.open.length > output.def.open.length) { + //This token's opening tag is more specific than the previous match + output.position = first_key_position; + output.def = token_template; + output.close_position = close_key_position; + } else if (token_template.open.length === output.def.open.length) { + if (token_template.close.length > output.def.close.length) { + //This token's opening tag is as specific as the previous match, + //but the closing tag has greater specificity + if (close_key_position >= 0 && close_key_position < output.close_position) { + //This token's closing tag exists in the template, + //and it occurs sooner than the previous match + output.position = first_key_position; + output.def = token_template; + output.close_position = close_key_position; + } + } else if (close_key_position >= 0 && close_key_position < output.close_position) { + //This token's closing tag is not more specific than the previous match, + //but it occurs sooner than the previous match + output.position = first_key_position; + output.def = token_template; + output.close_position = close_key_position; + } + } } } + delete output['close_position']; + return output; }; @@ -286,6 +402,11 @@ var Twig = (function (Twig) { if (token_def.type === Twig.token.type.comment) { break; } + // Ignore quotes within raw tag + // Fixes #283 + if (token_def.type === Twig.token.type.raw) { + break; + } l = Twig.token.strings.length; for (i = 0; i < l; i += 1) { @@ -361,9 +482,16 @@ var Twig = (function (Twig) { value: template.substring(0, end).trim() }); - if ( found_token.def.type === "logic" && template.substr( end + found_token.def.close.length, 1 ) === "\n" ) { - // Newlines directly after logic tokens are ignored - end += 1; + if (template.substr( end + found_token.def.close.length, 1 ) === "\n") { + switch (found_token.def.type) { + case "logic_whitespace_pre": + case "logic_whitespace_post": + case "logic_whitespace_both": + case "logic": + // Newlines directly after logic tokens are ignored + end += 1; + break; + } } template = template.substr(end + found_token.def.close.length); @@ -399,8 +527,14 @@ var Twig = (function (Twig) { unclosed_token = null, // Temporary previous token. prev_token = null, + // Temporary previous output. + prev_output = null, + // Temporary previous intermediate output. + prev_intermediate_output = null, // The previous token's template prev_template = null, + // Token lookahead + next_token = null, // The output token tok_output = null, @@ -409,8 +543,87 @@ var Twig = (function (Twig) { open = null, next = null; + var compile_output = function(token) { + Twig.expression.compile.apply(this, [token]); + if (stack.length > 0) { + intermediate_output.push(token); + } else { + output.push(token); + } + }; + + var compile_logic = function(token) { + // Compile the logic token + logic_token = Twig.logic.compile.apply(this, [token]); + + type = logic_token.type; + open = Twig.logic.handler[type].open; + next = Twig.logic.handler[type].next; + + Twig.log.trace("Twig.compile: ", "Compiled logic token to ", logic_token, + " next is: ", next, " open is : ", open); + + // Not a standalone token, check logic stack to see if this is expected + if (open !== undefined && !open) { + prev_token = stack.pop(); + prev_template = Twig.logic.handler[prev_token.type]; + + if (Twig.indexOf(prev_template.next, type) < 0) { + throw new Error(type + " not expected after a " + prev_token.type); + } + + prev_token.output = prev_token.output || []; + + prev_token.output = prev_token.output.concat(intermediate_output); + intermediate_output = []; + + tok_output = { + type: Twig.token.type.logic, + token: prev_token + }; + if (stack.length > 0) { + intermediate_output.push(tok_output); + } else { + output.push(tok_output); + } + } + + // This token requires additional tokens to complete the logic structure. + if (next !== undefined && next.length > 0) { + Twig.log.trace("Twig.compile: ", "Pushing ", logic_token, " to logic stack."); + + if (stack.length > 0) { + // Put any currently held output into the output list of the logic operator + // currently at the head of the stack before we push a new one on. + prev_token = stack.pop(); + prev_token.output = prev_token.output || []; + prev_token.output = prev_token.output.concat(intermediate_output); + stack.push(prev_token); + intermediate_output = []; + } + + // Push the new logic token onto the logic stack + stack.push(logic_token); + + } else if (open !== undefined && open) { + tok_output = { + type: Twig.token.type.logic, + token: logic_token + }; + // Standalone token (like {% set ... %} + if (stack.length > 0) { + intermediate_output.push(tok_output); + } else { + output.push(tok_output); + } + } + }; + while (tokens.length > 0) { token = tokens.shift(); + prev_output = output[output.length - 1]; + prev_intermediate_output = intermediate_output[intermediate_output.length - 1]; + next_token = tokens[0]; Twig.log.trace("Compiling token ", token); switch (token.type) { case Twig.token.type.raw: @@ -422,83 +635,84 @@ var Twig = (function (Twig) { break; case Twig.token.type.logic: - // Compile the logic token - logic_token = Twig.logic.compile.apply(this, [token]); - - type = logic_token.type; - open = Twig.logic.handler[type].open; - next = Twig.logic.handler[type].next; + compile_logic.call(this, token); + break; - Twig.log.trace("Twig.compile: ", "Compiled logic token to ", logic_token, - " next is: ", next, " open is : ", open); + // Do nothing, comments should be ignored + case Twig.token.type.comment: + break; - // Not a standalone token, check logic stack to see if this is expected - if (open !== undefined && !open) { - prev_token = stack.pop(); - prev_template = Twig.logic.handler[prev_token.type]; + case Twig.token.type.output: + compile_output.call(this, token); + break; - if (Twig.indexOf(prev_template.next, type) < 0) { - throw new Error(type + " not expected after a " + prev_token.type); + //Kill whitespace ahead and behind this token + case Twig.token.type.logic_whitespace_pre: + case Twig.token.type.logic_whitespace_post: + case Twig.token.type.logic_whitespace_both: + case Twig.token.type.output_whitespace_pre: + case Twig.token.type.output_whitespace_post: + case Twig.token.type.output_whitespace_both: + if (token.type !== Twig.token.type.output_whitespace_post && token.type !== Twig.token.type.logic_whitespace_post) { + if (prev_output) { + //If the previous output is raw, pop it off + if (prev_output.type === Twig.token.type.raw) { + output.pop(); + + //If the previous output is not just whitespace, trim it + if (prev_output.value.match(/^\s*$/) === null) { + prev_output.value = prev_output.value.trim(); + //Repush the previous output + output.push(prev_output); + } + } } - prev_token.output = prev_token.output || []; + if (prev_intermediate_output) { + //If the previous intermediate output is raw, pop it off + if (prev_intermediate_output.type === Twig.token.type.raw) { + intermediate_output.pop(); - prev_token.output = prev_token.output.concat(intermediate_output); - intermediate_output = []; - - tok_output = { - type: Twig.token.type.logic, - token: prev_token - }; - if (stack.length > 0) { - intermediate_output.push(tok_output); - } else { - output.push(tok_output); + //If the previous output is not just whitespace, trim it + if (prev_intermediate_output.value.match(/^\s*$/) === null) { + prev_intermediate_output.value = prev_intermediate_output.value.trim(); + //Repush the previous intermediate output + intermediate_output.push(prev_intermediate_output); + } + } } } - // This token requires additional tokens to complete the logic structure. - if (next !== undefined && next.length > 0) { - Twig.log.trace("Twig.compile: ", "Pushing ", logic_token, " to logic stack."); - - if (stack.length > 0) { - // Put any currently held output into the output list of the logic operator - // currently at the head of the stack before we push a new one on. - prev_token = stack.pop(); - prev_token.output = prev_token.output || []; - prev_token.output = prev_token.output.concat(intermediate_output); - stack.push(prev_token); - intermediate_output = []; - } + //Compile this token + switch (token.type) { + case Twig.token.type.output_whitespace_pre: + case Twig.token.type.output_whitespace_post: + case Twig.token.type.output_whitespace_both: + compile_output.call(this, token); + break; + case Twig.token.type.logic_whitespace_pre: + case Twig.token.type.logic_whitespace_post: + case Twig.token.type.logic_whitespace_both: + compile_logic.call(this, token); + break; + } - // Push the new logic token onto the logic stack - stack.push(logic_token); - - } else if (open !== undefined && open) { - tok_output = { - type: Twig.token.type.logic, - token: logic_token - }; - // Standalone token (like {% set ... %} - if (stack.length > 0) { - intermediate_output.push(tok_output); - } else { - output.push(tok_output); + if (token.type !== Twig.token.type.output_whitespace_pre && token.type !== Twig.token.type.logic_whitespace_pre) { + if (next_token) { + //If the next token is raw, shift it out + if (next_token.type === Twig.token.type.raw) { + tokens.shift(); + + //If the next token is not just whitespace, trim it + if (next_token.value.match(/^\s*$/) === null) { + next_token.value = next_token.value.trim(); + //Unshift the next token + tokens.unshift(next_token); + } + } } } - break; - - // Do nothing, comments should be ignored - case Twig.token.type.comment: - break; - case Twig.token.type.output: - Twig.expression.compile.apply(this, [token]); - if (stack.length > 0) { - intermediate_output.push(token); - } else { - output.push(token); - } break; } @@ -541,16 +755,12 @@ var Twig = (function (Twig) { chain = true, that = this; - // Default to an empty object if none provided - context = context || { }; - - Twig.forEach(tokens, function parseToken(token) { Twig.log.debug("Twig.parse: ", "Parsing token: ", token); switch (token.type) { case Twig.token.type.raw: - output.push(token.value); + output.push(Twig.filters.raw(token.value)); break; case Twig.token.type.logic: @@ -572,6 +782,10 @@ var Twig = (function (Twig) { // Do nothing, comments should be ignored break; + //Fall through whitespace to output + case Twig.token.type.output_whitespace_pre: + case Twig.token.type.output_whitespace_post: + case Twig.token.type.output_whitespace_both: case Twig.token.type.output: Twig.log.debug("Twig.parse: ", "Output token: ", token.stack); // Parse the given expression in the given context @@ -579,7 +793,7 @@ var Twig = (function (Twig) { break; } }); - return output.join(""); + return Twig.output.apply(this, [output]); } catch (ex) { Twig.log.error("Error parsing twig template " + this.id + ": "); if (ex.stack) { @@ -619,8 +833,51 @@ var Twig = (function (Twig) { return tokens; }; + /** + * Join the output token's stack and escape it if needed + * + * @param {Array} Output token's stack + * + * @return {string|String} Autoescaped output + */ + Twig.output = function(output) { + if (!this.options.autoescape) { + return output.join(""); + } + + var strategy = 'html'; + if(typeof this.options.autoescape == 'string') + strategy = this.options.autoescape; + + // [].map would be better but it's not supported by IE8- + var escaped_output = []; + Twig.forEach(output, function (str) { + if (str && (str.twig_markup !== true && str.twig_markup != strategy)) { + str = Twig.filters.escape(str, [ strategy ]); + } + escaped_output.push(str); + }); + return Twig.Markup(escaped_output.join("")); + } + // Namespace for template storage and retrieval Twig.Templates = { + /** + * Registered template loaders - use Twig.Templates.registerLoader to add supported loaders + * @type {Object} + */ + loaders: {}, + + /** + * Registered template parsers - use Twig.Templates.registerParser to add supported parsers + * @type {Object} + */ + parsers: {}, + + /** + * Cached / loaded templates + * @type {Object} + */ registry: {} }; @@ -635,12 +892,131 @@ var Twig = (function (Twig) { Twig.validateId = function(id) { if (id === "prototype") { throw new Twig.Error(id + " is not a valid twig identifier"); - } else if (Twig.Templates.registry.hasOwnProperty(id)) { + } else if (Twig.cache && Twig.Templates.registry.hasOwnProperty(id)) { throw new Twig.Error("There is already a template with the ID " + id); } return true; } + /** + * Register a template loader + * + * @example + * Twig.extend(function(Twig) { + * Twig.Templates.registerLoader('custom_loader', function(location, params, callback, error_callback) { + * // ... load the template ... + * params.data = loadedTemplateData; + * // create and return the template + * var template = new Twig.Template(params); + * if (typeof callback === 'function') { + * callback(template); + * } + * return template; + * }); + * }); + * + * @param {String} method_name The method this loader is intended for (ajax, fs) + * @param {Function} func The function to execute when loading the template + * @param {Object|undefined} scope Optional scope parameter to bind func to + * + * @throws Twig.Error + * + * @return {void} + */ + Twig.Templates.registerLoader = function(method_name, func, scope) { + if (typeof func !== 'function') { + throw new Twig.Error('Unable to add loader for ' + method_name + ': Invalid function reference given.'); + } + if (scope) { + func = func.bind(scope); + } + this.loaders[method_name] = func; + }; + + /** + * Remove a registered loader + * + * @param {String} method_name The method name for the loader you wish to remove + * + * @return {void} + */ + Twig.Templates.unRegisterLoader = function(method_name) { + if (this.isRegisteredLoader(method_name)) { + delete this.loaders[method_name]; + } + }; + + /** + * See if a loader is registered by its method name + * + * @param {String} method_name The name of the loader you are looking for + * + * @return {boolean} + */ + Twig.Templates.isRegisteredLoader = function(method_name) { + return this.loaders.hasOwnProperty(method_name); + }; + + /** + * Register a template parser + * + * @example + * Twig.extend(function(Twig) { + * Twig.Templates.registerParser('custom_parser', function(params) { + * // this template source can be accessed in params.data + * var template = params.data + * + * // ... custom process that modifies the template + * + * // return the parsed template + * return template; + * }); + * }); + * + * @param {String} method_name The method this parser is intended for (twig, source) + * @param {Function} func The function to execute when parsing the template + * @param {Object|undefined} scope Optional scope parameter to bind func to + * + * @throws Twig.Error + * + * @return {void} + */ + Twig.Templates.registerParser = function(method_name, func, scope) { + if (typeof func !== 'function') { + throw new Twig.Error('Unable to add parser for ' + method_name + ': Invalid function regerence given.'); + } + + if (scope) { + func = func.bind(scope); + } + + this.parsers[method_name] = func; + }; + + /** + * Remove a registered parser + * + * @param {String} method_name The method name for the parser you wish to remove + * + * @return {void} + */ + Twig.Templates.unRegisterParser = function(method_name) { + if (this.isRegisteredParser(method_name)) { + delete this.parsers[method_name]; + } + }; + + /** + * See if a parser is registered by its method name + * + * @param {String} method_name The name of the parser you are looking for + * + * @return {boolean} + */ + Twig.Templates.isRegisteredParser = function(method_name) { + return this.parsers.hasOwnProperty(method_name); + }; + /** * Save a template object to the store. * @@ -676,6 +1052,8 @@ var Twig = (function (Twig) { * Defaults to true. * method: What method should be used to load the template * (fs or ajax) + * parser: What method should be used to parse the template + * (twig or source) * precompiled: Has the template already been compiled. * * @param {string} location The remote URL to load as a template. @@ -686,119 +1064,34 @@ var Twig = (function (Twig) { * */ Twig.Templates.loadRemote = function(location, params, callback, error_callback) { - var id = params.id, - method = params.method, - async = params.async, - precompiled = params.precompiled, - template = null; + var loader; // Default to async - if (async === undefined) async = true; + if (params.async === undefined) { + params.async = true; + } // Default to the URL so the template is cached. - if (id === undefined) { - id = location; + if (params.id === undefined) { + params.id = location; } - params.id = id; // Check for existing template - if (Twig.cache && Twig.Templates.registry.hasOwnProperty(id)) { + if (Twig.cache && Twig.Templates.registry.hasOwnProperty(params.id)) { // A template is already saved with the given id. - if (callback) { - callback(Twig.Templates.registry[id]); + if (typeof callback === 'function') { + callback(Twig.Templates.registry[params.id]); } - return Twig.Templates.registry[id]; + // TODO: if async, return deferred promise + return Twig.Templates.registry[params.id]; } - if (method == 'ajax') { - if (typeof XMLHttpRequest == "undefined") { - throw new Twig.Error("Unsupported platform: Unable to do remote requests " + - "because there is no XMLHTTPRequest implementation"); - } - - var xmlhttp = new XMLHttpRequest(); - xmlhttp.onreadystatechange = function() { - var data = null; - - if(xmlhttp.readyState == 4) { - if (xmlhttp.status == 200) { - Twig.log.debug("Got template ", xmlhttp.responseText); - - if (precompiled === true) { - data = JSON.parse(xmlhttp.responseText); - } else { - data = xmlhttp.responseText; - } - - params.url = location; - params.data = data; - - template = new Twig.Template(params); - - if (callback) { - callback(template); - } - } else { - if (error_callback) { - error_callback(xmlhttp); - } - } - } - }; - xmlhttp.open("GET", location, async); - xmlhttp.send(); - - } else { // if method = 'fs' - // Create local scope - (function() { - var fs = require('fs'), - path = require('path'), - data = null, - loadTemplateFn = function(err, data) { - if (err) { - if (error_callback) { - error_callback(err); - } - return; - } - - if (precompiled === true) { - data = JSON.parse(data); - } - - params.data = data; - params.path = location; + //if the parser name hasn't been set, default it to twig + params.parser = params.parser || 'twig'; - // template is in data - template = new Twig.Template(params); - - if (callback) { - callback(template); - } - }; - - if (async === true) { - fs.stat(location, function (err, stats) { - if (err || !stats.isFile()) - throw new Twig.Error("Unable to find template file " + location); - - fs.readFile(location, 'utf8', loadTemplateFn); - }); - } else { - if (!fs.statSync(location).isFile()) - throw new Twig.Error("Unable to find template file " + location); - - data = fs.readFileSync(location, 'utf8'); - loadTemplateFn(undefined, data); - } - })(); - } - if (async === false) { - return template; - } else { - // placeholder for now, should eventually return a deferred object. - return true; - } + // Assume 'fs' if the loader is not defined + loader = this.loaders[params.method] || this.loaders.fs; + return loader.apply(this, arguments); }; // Determine object type @@ -826,6 +1119,8 @@ var Twig = (function (Twig) { base = params.base, path = params.path, url = params.url, + name = params.name, + method = params.method, // parser options options = params.options; @@ -841,16 +1136,18 @@ var Twig = (function (Twig) { // options: { // Compiler/parser options // - // strict_variables: true/false + // strictVariables: true/false // Should missing variable/keys emit an error message. If false, they default to null. // } // } // this.id = id; + this.method = method; this.base = base; this.path = path; this.url = url; + this.name = name; this.macros = macros; this.options = options; @@ -870,6 +1167,8 @@ var Twig = (function (Twig) { Twig.Template.prototype.reset = function(blocks) { Twig.log.debug("Twig.Template.reset", "Reseting template " + this.id); this.blocks = {}; + this.importedBlocks = []; + this.originalBlockTokens = {}; this.child = { blocks: blocks || {} }; @@ -909,10 +1208,10 @@ var Twig = (function (Twig) { // check for the template file via include if (!ext_template) { - url = relativePath(this, this.extend); + url = Twig.path.parsePath(this, this.extend); ext_template = Twig.Templates.loadRemote(url, { - method: this.url?'ajax':'fs', + method: this.getLoaderMethod(), base: this.base, async: false, id: url, @@ -938,21 +1237,33 @@ var Twig = (function (Twig) { Twig.Template.prototype.importFile = function(file) { var url, sub_template; - if ( !this.url && !this.path && this.options.allowInlineIncludes ) { + if (!this.url && this.options.allowInlineIncludes) { + file = this.path ? this.path + '/' + file : file; sub_template = Twig.Templates.load(file); - sub_template.options = this.options; - if ( sub_template ) { - return sub_template; + + if (!sub_template) { + sub_template = Twig.Templates.loadRemote(url, { + id: file, + method: this.getLoaderMethod(), + async: false, + options: this.options + }); + + if (!sub_template) { + throw new Twig.Error("Unable to find the template " + file); + } } - throw new Twig.Error("Didn't find the inline template by id"); + sub_template.options = this.options; + + return sub_template; } - url = relativePath(this, file); + url = Twig.path.parsePath(this, file); // Load blocks from an external file sub_template = Twig.Templates.loadRemote(url, { - method: this.url?'ajax':'fs', + method: this.getLoaderMethod(), base: this.base, async: false, options: this.options, @@ -976,16 +1287,17 @@ var Twig = (function (Twig) { Twig.forEach(Object.keys(sub_template.blocks), function(key) { if (override || that.blocks[key] === undefined) { that.blocks[key] = sub_template.blocks[key]; + that.importedBlocks.push(key); } }); }; Twig.Template.prototype.importMacros = function(file) { - var url = relativePath(this, file); + var url = Twig.path.parsePath(this, file); // load remote template var remoteTemplate = Twig.Templates.loadRemote(url, { - method: this.url?'ajax':'fs', + method: this.getLoaderMethod(), async: false, id: url }); @@ -993,76 +1305,181 @@ var Twig = (function (Twig) { return remoteTemplate; }; + Twig.Template.prototype.getLoaderMethod = function() { + if (this.path) { + return 'fs'; + } + if (this.url) { + return 'ajax'; + } + return this.method || 'fs'; + }; + Twig.Template.prototype.compile = function(options) { // compile the template into raw JS return Twig.compiler.compile(this, options); }; /** - * Generate the relative canonical version of a url based on the given base path and file path. + * Create safe output * - * @param {string} template The Twig.Template. - * @param {string} file The file path, relative to the base path. + * @param {string} Content safe to output * - * @return {string} The canonical version of the path. + * @return {String} Content wrapped into a String */ - function relativePath(template, file) { - var base, - base_path, - sep_chr = "/", - new_path = [], - val; - if (template.url) { - if (typeof template.base !== 'undefined') { - base = template.base + ((template.base.charAt(template.base.length-1) === '/') ? '' : '/'); - } else { - base = template.url; - } - } else if (template.path) { - // Get the system-specific path separator - var path = require("path"), - sep = path.sep || sep_chr, - relative = new RegExp("^\\.{1,2}" + sep.replace("\\", "\\\\")); - file = file.replace(/\//g, sep); + Twig.Markup = function(content, strategy) { + if(typeof strategy == 'undefined') { + strategy = true; + } - if (template.base !== undefined && file.match(relative) == null) { - file = file.replace(template.base, ''); - base = template.base + sep; - } else { - base = template.path; - } + if (typeof content === 'string' && content.length > 0) { + content = new String(content); + content.twig_markup = strategy; + } + return content; + }; - base = base.replace(sep+sep, sep); - sep_chr = sep; - } else { - throw new Twig.Error("Cannot extend an inline template."); + return Twig; + +}) (Twig || { }); + +(function(Twig) { + + 'use strict'; + + Twig.Templates.registerLoader('ajax', function(location, params, callback, error_callback) { + var template, + xmlhttp, + precompiled = params.precompiled, + parser = this.parsers[params.parser] || this.parser.twig; + + if (typeof XMLHttpRequest === "undefined") { + throw new Twig.Error('Unsupported platform: Unable to do ajax requests ' + + 'because there is no "XMLHTTPRequest" implementation'); } - base_path = base.split(sep_chr); + xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + var data = null; - // Remove file from url - base_path.pop(); - base_path = base_path.concat(file.split(sep_chr)); + if(xmlhttp.readyState === 4) { + if (xmlhttp.status === 200 || (window.cordova && xmlhttp.status == 0)) { + Twig.log.debug("Got template ", xmlhttp.responseText); - while (base_path.length > 0) { - val = base_path.shift(); - if (val == ".") { - // Ignore - } else if (val == ".." && new_path.length > 0 && new_path[new_path.length-1] != "..") { - new_path.pop(); - } else { - new_path.push(val); + if (precompiled === true) { + data = JSON.parse(xmlhttp.responseText); + } else { + data = xmlhttp.responseText; + } + + params.url = location; + params.data = data; + + template = parser.call(this, params); + + if (typeof callback === 'function') { + callback(template); + } + } else { + if (typeof error_callback === 'function') { + error_callback(xmlhttp); + } + } } + }; + xmlhttp.open("GET", location, !!params.async); + xmlhttp.send(); + + if (params.async) { + // TODO: return deferred promise + return true; + } else { + return template; } + }); - return new_path.join(sep_chr); +}(Twig));(function(Twig) { + 'use strict'; + + var fs, path; + + try { + // require lib dependencies at runtime + fs = require('fs'); + path = require('path'); + } catch (e) { + // NOTE: this is in a try/catch to avoid errors cross platform } - return Twig; + Twig.Templates.registerLoader('fs', function(location, params, callback, error_callback) { + var template, + data = null, + precompiled = params.precompiled, + parser = this.parsers[params.parser] || this.parser.twig; -}) (Twig || { }); + if (!fs || !path) { + throw new Twig.Error('Unsupported platform: Unable to load from file ' + + 'because there is no "fs" or "path" implementation'); + } + + var loadTemplateFn = function(err, data) { + if (err) { + if (typeof error_callback === 'function') { + error_callback(err); + } + return; + } + + if (precompiled === true) { + data = JSON.parse(data); + } + + params.data = data; + params.path = params.path || location; + // template is in data + template = parser.call(this, params); + + if (typeof callback === 'function') { + callback(template); + } + }; + params.path = params.path || location; + + if (params.async) { + fs.stat(params.path, function (err, stats) { + if (err || !stats.isFile()) { + throw new Twig.Error('Unable to find template file ' + location); + } + fs.readFile(params.path, 'utf8', loadTemplateFn); + }); + // TODO: return deferred promise + return true; + } else { + if (!fs.statSync(params.path).isFile()) { + throw new Twig.Error('Unable to find template file ' + location); + } + data = fs.readFileSync(params.path, 'utf8'); + loadTemplateFn(undefined, data); + return template + } + }); + +}(Twig));(function(Twig){ + 'use strict'; + + Twig.Templates.registerParser('source', function(params) { + return params.data || ''; + }); +})(Twig); +(function(Twig){ + 'use strict'; + + Twig.Templates.registerParser('twig', function(params) { + return new Twig.Template(params); + }); +})(Twig); // The following methods are from MDN and are available under a // [MIT License](http://www.opensource.org/licenses/mit-license.php) or are // [Public Domain](https://developer.mozilla.org/Project:Copyrights). @@ -1107,132 +1524,207 @@ var Twig = (function(Twig) { Twig.lib = { }; /** - sprintf() for JavaScript 0.7-beta1 - http://www.diveintojavascript.com/projects/javascript-sprintf + sprintf() for JavaScript 1.0.3 + https://github.com/alexei/sprintf.js **/ - var sprintf = (function() { - function get_type(variable) { - return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase(); - } - function str_repeat(input, multiplier) { - for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */} - return output.join(''); + var sprintfLib = (function() { + var re = { + not_string: /[^s]/, + number: /[diefg]/, + json: /[j]/, + not_json: /[^j]/, + text: /^[^\x25]+/, + modulo: /^\x25{2}/, + placeholder: /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijosuxX])/, + key: /^([a-z_][a-z_\d]*)/i, + key_access: /^\.([a-z_][a-z_\d]*)/i, + index_access: /^\[(\d+)\]/, + sign: /^[\+\-]/ + } + + function sprintf() { + var key = arguments[0], cache = sprintf.cache + if (!(cache[key] && cache.hasOwnProperty(key))) { + cache[key] = sprintf.parse(key) } + return sprintf.format.call(null, cache[key], arguments) + } + + sprintf.format = function(parse_tree, argv) { + var cursor = 1, tree_length = parse_tree.length, node_type = "", arg, output = [], i, k, match, pad, pad_character, pad_length, is_positive = true, sign = "" + for (i = 0; i < tree_length; i++) { + node_type = get_type(parse_tree[i]) + if (node_type === "string") { + output[output.length] = parse_tree[i] + } + else if (node_type === "array") { + match = parse_tree[i] // convenience purposes only + if (match[2]) { // keyword argument + arg = argv[cursor] + for (k = 0; k < match[2].length; k++) { + if (!arg.hasOwnProperty(match[2][k])) { + throw new Error(sprintf("[sprintf] property '%s' does not exist", match[2][k])) + } + arg = arg[match[2][k]] + } + } + else if (match[1]) { // positional argument (explicit) + arg = argv[match[1]] + } + else { // positional argument (implicit) + arg = argv[cursor++] + } - var str_format = function() { - if (!str_format.cache.hasOwnProperty(arguments[0])) { - str_format.cache[arguments[0]] = str_format.parse(arguments[0]); + if (get_type(arg) == "function") { + arg = arg() } - return str_format.format.call(null, str_format.cache[arguments[0]], arguments); - }; - str_format.format = function(parse_tree, argv) { - var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length; - for (i = 0; i < tree_length; i++) { - node_type = get_type(parse_tree[i]); - if (node_type === 'string') { - output.push(parse_tree[i]); - } - else if (node_type === 'array') { - match = parse_tree[i]; // convenience purposes only - if (match[2]) { // keyword argument - arg = argv[cursor]; - for (k = 0; k < match[2].length; k++) { - if (!arg.hasOwnProperty(match[2][k])) { - throw(sprintf('[sprintf] property "%s" does not exist', match[2][k])); - } - arg = arg[match[2][k]]; - } - } - else if (match[1]) { // positional argument (explicit) - arg = argv[match[1]]; - } - else { // positional argument (implicit) - arg = argv[cursor++]; - } + if (re.not_string.test(match[8]) && re.not_json.test(match[8]) && (get_type(arg) != "number" && isNaN(arg))) { + throw new TypeError(sprintf("[sprintf] expecting number but found %s", get_type(arg))) + } + + if (re.number.test(match[8])) { + is_positive = arg >= 0 + } + + switch (match[8]) { + case "b": + arg = arg.toString(2) + break + case "c": + arg = String.fromCharCode(arg) + break + case "d": + case "i": + arg = parseInt(arg, 10) + break + case "j": + arg = JSON.stringify(arg, null, match[6] ? parseInt(match[6]) : 0) + break + case "e": + arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential() + break + case "f": + arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg) + break + case "g": + arg = match[7] ? parseFloat(arg).toPrecision(match[7]) : parseFloat(arg) + break + case "o": + arg = arg.toString(8) + break + case "s": + arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg) + break + case "u": + arg = arg >>> 0 + break + case "x": + arg = arg.toString(16) + break + case "X": + arg = arg.toString(16).toUpperCase() + break + } + if (re.json.test(match[8])) { + output[output.length] = arg + } + else { + if (re.number.test(match[8]) && (!is_positive || match[3])) { + sign = is_positive ? "+" : "-" + arg = arg.toString().replace(re.sign, "") + } + else { + sign = "" + } + pad_character = match[4] ? match[4] === "0" ? "0" : match[4].charAt(1) : " " + pad_length = match[6] - (sign + arg).length + pad = match[6] ? (pad_length > 0 ? str_repeat(pad_character, pad_length) : "") : "" + output[output.length] = match[5] ? sign + arg + pad : (pad_character === "0" ? sign + pad + arg : pad + sign + arg) + } + } + } + return output.join("") + } + + sprintf.cache = {} - if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) { - throw(sprintf('[sprintf] expecting number but found %s', get_type(arg))); - } - switch (match[8]) { - case 'b': arg = arg.toString(2); break; - case 'c': arg = String.fromCharCode(arg); break; - case 'd': arg = parseInt(arg, 10); break; - case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break; - case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break; - case 'o': arg = arg.toString(8); break; - case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break; - case 'u': arg = Math.abs(arg); break; - case 'x': arg = arg.toString(16); break; - case 'X': arg = arg.toString(16).toUpperCase(); break; - } - arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg); - pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' '; - pad_length = match[6] - String(arg).length; - pad = match[6] ? str_repeat(pad_character, pad_length) : ''; - output.push(match[5] ? arg + pad : pad + arg); + sprintf.parse = function(fmt) { + var _fmt = fmt, match = [], parse_tree = [], arg_names = 0 + while (_fmt) { + if ((match = re.text.exec(_fmt)) !== null) { + parse_tree[parse_tree.length] = match[0] + } + else if ((match = re.modulo.exec(_fmt)) !== null) { + parse_tree[parse_tree.length] = "%" + } + else if ((match = re.placeholder.exec(_fmt)) !== null) { + if (match[2]) { + arg_names |= 1 + var field_list = [], replacement_field = match[2], field_match = [] + if ((field_match = re.key.exec(replacement_field)) !== null) { + field_list[field_list.length] = field_match[1] + while ((replacement_field = replacement_field.substring(field_match[0].length)) !== "") { + if ((field_match = re.key_access.exec(replacement_field)) !== null) { + field_list[field_list.length] = field_match[1] + } + else if ((field_match = re.index_access.exec(replacement_field)) !== null) { + field_list[field_list.length] = field_match[1] + } + else { + throw new SyntaxError("[sprintf] failed to parse named argument key") + } } + } + else { + throw new SyntaxError("[sprintf] failed to parse named argument key") + } + match[2] = field_list } - return output.join(''); - }; + else { + arg_names |= 2 + } + if (arg_names === 3) { + throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported") + } + parse_tree[parse_tree.length] = match + } + else { + throw new SyntaxError("[sprintf] unexpected placeholder") + } + _fmt = _fmt.substring(match[0].length) + } + return parse_tree + } - str_format.cache = {}; + var vsprintf = function(fmt, argv, _argv) { + _argv = (argv || []).slice(0) + _argv.splice(0, 0, fmt) + return sprintf.apply(null, _argv) + } - str_format.parse = function(fmt) { - var _fmt = fmt, match = [], parse_tree = [], arg_names = 0; - while (_fmt) { - if ((match = /^[^\x25]+/.exec(_fmt)) !== null) { - parse_tree.push(match[0]); - } - else if ((match = /^\x25{2}/.exec(_fmt)) !== null) { - parse_tree.push('%'); - } - else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) { - if (match[2]) { - arg_names |= 1; - var field_list = [], replacement_field = match[2], field_match = []; - if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { - field_list.push(field_match[1]); - while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') { - if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { - field_list.push(field_match[1]); - } - else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) { - field_list.push(field_match[1]); - } - else { - throw('[sprintf] huh?'); - } - } - } - else { - throw('[sprintf] huh?'); - } - match[2] = field_list; - } - else { - arg_names |= 2; - } - if (arg_names === 3) { - throw('[sprintf] mixing positional and named placeholders is not (yet) supported'); - } - parse_tree.push(match); - } - else { - throw('[sprintf] huh?'); - } - _fmt = _fmt.substring(match[0].length); - } - return parse_tree; - }; + /** + * helpers + */ + function get_type(variable) { + return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase() + } - return str_format; + function str_repeat(input, multiplier) { + return Array(multiplier + 1).join(input) + } + + /** + * export + */ + return { + sprintf: sprintf, + vsprintf: vsprintf + } })(); - var vsprintf = function(fmt, argv) { - argv.unshift(fmt); - return sprintf.apply(null, argv); - }; + var sprintf = sprintfLib.sprintf; + var vsprintf = sprintfLib.vsprintf; // Expose to Twig Twig.lib.sprintf = sprintf; @@ -1443,8 +1935,8 @@ var Twig = (function(Twig) { Twig.lib.parseISO8601Date = function (s){ // Taken from http://n8v.enteuxis.org/2010/12/parsing-iso-8601-dates-in-javascript/ // parenthese matches: - // year month day hours minutes seconds - // dotmilliseconds + // year month day hours minutes seconds + // dotmilliseconds // tzstring plusminus hours minutes var re = /(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(\.\d+)?(Z|([+-])(\d\d):(\d\d))/; @@ -1455,7 +1947,7 @@ var Twig = (function(Twig) { // ["2010-12-07T11:00:00.000-09:00", "2010", "12", "07", "11", // "00", "00", ".000", "-09:00", "-", "09", "00"] // "2010-12-07T11:00:00.000Z" parses to: - // ["2010-12-07T11:00:00.000Z", "2010", "12", "07", "11", + // ["2010-12-07T11:00:00.000Z", "2010", "12", "07", "11", // "00", "00", ".000", "Z", undefined, undefined, undefined] if (! d) { @@ -1475,7 +1967,7 @@ var Twig = (function(Twig) { var ms = Date.UTC(d[1], d[2] - 1, d[3], d[4], d[5], d[6]); // if there are milliseconds, add them - if (d[7] > 0) { + if (d[7] > 0) { ms += Math.round(d[7] * 1000); } @@ -1496,196 +1988,300 @@ var Twig = (function(Twig) { return new Date(ms); }; - Twig.lib.strtotime = function (str, now) { - // http://kevin.vanzonneveld.net - // + original by: Caio Ariede (http://caioariede.com) - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: David - // + improved by: Caio Ariede (http://caioariede.com) - // + improved by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Wagner B. Soares - // + bugfixed by: Artur Tchernychev - // % note 1: Examples all have a fixed timestamp to prevent tests to fail because of variable time(zones) - // * example 1: strtotime('+1 day', 1129633200); - // * returns 1: 1129719600 - // * example 2: strtotime('+1 week 2 days 4 hours 2 seconds', 1129633200); - // * returns 2: 1130425202 - // * example 3: strtotime('last month', 1129633200); - // * returns 3: 1127041200 - // * example 4: strtotime('2009-05-04 08:30:00'); - // * returns 4: 1241418600 - var i, l, match, s, parse = ''; - - str = str.replace(/\s{2,}|^\s|\s$/g, ' '); // unecessary spaces - str = str.replace(/[\t\r\n]/g, ''); // unecessary chars - if (str === 'now') { - return now === null || isNaN(now) ? new Date().getTime() / 1000 | 0 : now | 0; - } else if (!isNaN(parse = Date.parse(str))) { - return parse / 1000 | 0; - } else if (now) { - now = new Date(now * 1000); // Accept PHP-style seconds - } else { - now = new Date(); - } - - var upperCaseStr = str; - - str = str.toLowerCase(); - - var __is = { - day: { - 'sun': 0, - 'mon': 1, - 'tue': 2, - 'wed': 3, - 'thu': 4, - 'fri': 5, - 'sat': 6 - }, - mon: [ - 'jan', - 'feb', - 'mar', - 'apr', - 'may', - 'jun', - 'jul', - 'aug', - 'sep', - 'oct', - 'nov', - 'dec' - ] - }; - - var process = function (m) { - var ago = (m[2] && m[2] === 'ago'); - var num = (num = m[0] === 'last' ? -1 : 1) * (ago ? -1 : 1); - - switch (m[0]) { - case 'last': - case 'next': - switch (m[1].substring(0, 3)) { - case 'yea': - now.setFullYear(now.getFullYear() + num); - break; - case 'wee': - now.setDate(now.getDate() + (num * 7)); - break; - case 'day': - now.setDate(now.getDate() + num); - break; - case 'hou': - now.setHours(now.getHours() + num); - break; - case 'min': - now.setMinutes(now.getMinutes() + num); - break; - case 'sec': - now.setSeconds(now.getSeconds() + num); - break; - case 'mon': - if (m[1] === "month") { - now.setMonth(now.getMonth() + num); - break; - } - // fall through - default: - var day = __is.day[m[1].substring(0, 3)]; - if (typeof day !== 'undefined') { - var diff = day - now.getDay(); - if (diff === 0) { - diff = 7 * num; - } else if (diff > 0) { - if (m[0] === 'last') { - diff -= 7; - } - } else { - if (m[0] === 'next') { - diff += 7; - } - } - now.setDate(now.getDate() + diff); - now.setHours(0, 0, 0, 0); // when jumping to a specific last/previous day of week, PHP sets the time to 00:00:00 - } - } - break; - - default: - if (/\d+/.test(m[0])) { - num *= parseInt(m[0], 10); - - switch (m[1].substring(0, 3)) { - case 'yea': - now.setFullYear(now.getFullYear() + num); - break; - case 'mon': - now.setMonth(now.getMonth() + num); - break; - case 'wee': - now.setDate(now.getDate() + (num * 7)); - break; - case 'day': - now.setDate(now.getDate() + num); - break; - case 'hou': - now.setHours(now.getHours() + num); - break; - case 'min': - now.setMinutes(now.getMinutes() + num); - break; - case 'sec': - now.setSeconds(now.getSeconds() + num); - break; - } - } else { - return false; - } - break; - } - return true; - }; - - match = str.match(/^(\d{2,4}-\d{2}-\d{2})(?:\s(\d{1,2}:\d{2}(:\d{2})?)?(?:\.(\d+))?)?$/); - if (match !== null) { - if (!match[2]) { - match[2] = '00:00:00'; - } else if (!match[3]) { - match[2] += ':00'; - } - - s = match[1].split(/-/g); - - s[1] = __is.mon[s[1] - 1] || s[1]; - s[0] = +s[0]; - - s[0] = (s[0] >= 0 && s[0] <= 69) ? '20' + (s[0] < 10 ? '0' + s[0] : s[0] + '') : (s[0] >= 70 && s[0] <= 99) ? '19' + s[0] : s[0] + ''; - return parseInt(this.strtotime(s[2] + ' ' + s[1] + ' ' + s[0] + ' ' + match[2]) + (match[4] ? match[4] / 1000 : ''), 10); - } - - var regex = '([+-]?\\d+\\s' + '(years?|months?|weeks?|days?|hours?|min|minutes?|sec|seconds?' + '|sun\\.?|sunday|mon\\.?|monday|tue\\.?|tuesday|wed\\.?|wednesday' + '|thu\\.?|thursday|fri\\.?|friday|sat\\.?|saturday)' + '|(last|next)\\s' + '(years?|months?|weeks?|days?|hours?|min|minutes?|sec|seconds?' + '|sun\\.?|sunday|mon\\.?|monday|tue\\.?|tuesday|wed\\.?|wednesday' + '|thu\\.?|thursday|fri\\.?|friday|sat\\.?|saturday))' + '(\\sago)?'; - - match = str.match(new RegExp(regex, 'gi')); // Brett: seems should be case insensitive per docs, so added 'i' - if (match === null) { - // Try to parse ISO8601 in IE8 - try { - num = Twig.lib.parseISO8601Date(upperCaseStr); - if (num) { - return num / 1000 | 0; - } - } catch (err) { - return false; - } - return false; - } - - for (i = 0, l = match.length; i < l; i++) { - if (!process(match[i].split(' '))) { - return false; - } - } - - return now.getTime() / 1000 | 0; + Twig.lib.strtotime = function (text, now) { + // discuss at: http://phpjs.org/functions/strtotime/ + // version: 1109.2016 + // original by: Caio Ariede (http://caioariede.com) + // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // improved by: Caio Ariede (http://caioariede.com) + // improved by: A. Matías Quezada (http://amatiasq.com) + // improved by: preuter + // improved by: Brett Zamir (http://brett-zamir.me) + // improved by: Mirko Faber + // input by: David + // bugfixed by: Wagner B. Soares + // bugfixed by: Artur Tchernychev + // bugfixed by: Stephan Bösch-Plepelits (http://github.com/plepe) + // note: Examples all have a fixed timestamp to prevent tests to fail because of variable time(zones) + // example 1: strtotime('+1 day', 1129633200); + // returns 1: 1129719600 + // example 2: strtotime('+1 week 2 days 4 hours 2 seconds', 1129633200); + // returns 2: 1130425202 + // example 3: strtotime('last month', 1129633200); + // returns 3: 1127041200 + // example 4: strtotime('2009-05-04 08:30:00 GMT'); + // returns 4: 1241425800 + // example 5: strtotime('2009-05-04 08:30:00+00'); + // returns 5: 1241425800 + // example 6: strtotime('2009-05-04 08:30:00+02:00'); + // returns 6: 1241418600 + // example 7: strtotime('2009-05-04T08:30:00Z'); + // returns 7: 1241425800 + + var parsed, match, today, year, date, days, ranges, len, times, regex, i, fail = false; + + if (!text) { + return fail; + } + + // Unecessary spaces + text = text.replace(/^\s+|\s+$/g, '') + .replace(/\s{2,}/g, ' ') + .replace(/[\t\r\n]/g, '') + .toLowerCase(); + + // in contrast to php, js Date.parse function interprets: + // dates given as yyyy-mm-dd as in timezone: UTC, + // dates with "." or "-" as MDY instead of DMY + // dates with two-digit years differently + // etc...etc... + // ...therefore we manually parse lots of common date formats + match = text.match( + /^(\d{1,4})([\-\.\/\:])(\d{1,2})([\-\.\/\:])(\d{1,4})(?:\s(\d{1,2}):(\d{2})?:?(\d{2})?)?(?:\s([A-Z]+)?)?$/); + + if (match && match[2] === match[4]) { + if (match[1] > 1901) { + switch (match[2]) { + case '-': + { + // YYYY-M-D + if (match[3] > 12 || match[5] > 31) { + return fail; + } + + return new Date(match[1], parseInt(match[3], 10) - 1, match[5], + match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; + } + case '.': + { + // YYYY.M.D is not parsed by strtotime() + return fail; + } + case '/': + { + // YYYY/M/D + if (match[3] > 12 || match[5] > 31) { + return fail; + } + + return new Date(match[1], parseInt(match[3], 10) - 1, match[5], + match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; + } + } + } else if (match[5] > 1901) { + switch (match[2]) { + case '-': + { + // D-M-YYYY + if (match[3] > 12 || match[1] > 31) { + return fail; + } + + return new Date(match[5], parseInt(match[3], 10) - 1, match[1], + match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; + } + case '.': + { + // D.M.YYYY + if (match[3] > 12 || match[1] > 31) { + return fail; + } + + return new Date(match[5], parseInt(match[3], 10) - 1, match[1], + match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; + } + case '/': + { + // M/D/YYYY + if (match[1] > 12 || match[3] > 31) { + return fail; + } + + return new Date(match[5], parseInt(match[1], 10) - 1, match[3], + match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; + } + } + } else { + switch (match[2]) { + case '-': + { + // YY-M-D + if (match[3] > 12 || match[5] > 31 || (match[1] < 70 && match[1] > 38)) { + return fail; + } + + year = match[1] >= 0 && match[1] <= 38 ? +match[1] + 2000 : match[1]; + return new Date(year, parseInt(match[3], 10) - 1, match[5], + match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; + } + case '.': + { + // D.M.YY or H.MM.SS + if (match[5] >= 70) { + // D.M.YY + if (match[3] > 12 || match[1] > 31) { + return fail; + } + + return new Date(match[5], parseInt(match[3], 10) - 1, match[1], + match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; + } + if (match[5] < 60 && !match[6]) { + // H.MM.SS + if (match[1] > 23 || match[3] > 59) { + return fail; + } + + today = new Date(); + return new Date(today.getFullYear(), today.getMonth(), today.getDate(), + match[1] || 0, match[3] || 0, match[5] || 0, match[9] || 0) / 1000; + } + + // invalid format, cannot be parsed + return fail; + } + case '/': + { + // M/D/YY + if (match[1] > 12 || match[3] > 31 || (match[5] < 70 && match[5] > 38)) { + return fail; + } + + year = match[5] >= 0 && match[5] <= 38 ? +match[5] + 2000 : match[5]; + return new Date(year, parseInt(match[1], 10) - 1, match[3], + match[6] || 0, match[7] || 0, match[8] || 0, match[9] || 0) / 1000; + } + case ':': + { + // HH:MM:SS + if (match[1] > 23 || match[3] > 59 || match[5] > 59) { + return fail; + } + + today = new Date(); + return new Date(today.getFullYear(), today.getMonth(), today.getDate(), + match[1] || 0, match[3] || 0, match[5] || 0) / 1000; + } + } + } + } + + // other formats and "now" should be parsed by Date.parse() + if (text === 'now') { + return now === null || isNaN(now) ? new Date() + .getTime() / 1000 | 0 : now | 0; + } + if (!isNaN(parsed = Date.parse(text))) { + return parsed / 1000 | 0; + } + // Browsers != Chrome have problems parsing ISO 8601 date strings, as they do + // not accept lower case characters, space, or shortened time zones. + // Therefore, fix these problems and try again. + // Examples: + // 2015-04-15 20:33:59+02 + // 2015-04-15 20:33:59z + // 2015-04-15t20:33:59+02:00 + if (match = text.match(/^([0-9]{4}-[0-9]{2}-[0-9]{2})[ t]([0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?)([\+-][0-9]{2}(:[0-9]{2})?|z)/)) { + // fix time zone information + if (match[4] == 'z') { + match[4] = 'Z'; + } + else if (match[4].match(/^([\+-][0-9]{2})$/)) { + match[4] = match[4] + ':00'; + } + + if (!isNaN(parsed = Date.parse(match[1] + 'T' + match[2] + match[4]))) { + return parsed / 1000 | 0; + } + } + + date = now ? new Date(now * 1000) : new Date(); + days = { + 'sun': 0, + 'mon': 1, + 'tue': 2, + 'wed': 3, + 'thu': 4, + 'fri': 5, + 'sat': 6 + }; + ranges = { + 'yea': 'FullYear', + 'mon': 'Month', + 'day': 'Date', + 'hou': 'Hours', + 'min': 'Minutes', + 'sec': 'Seconds' + }; + + function lastNext(type, range, modifier) { + var diff, day = days[range]; + + if (typeof day !== 'undefined') { + diff = day - date.getDay(); + + if (diff === 0) { + diff = 7 * modifier; + } else if (diff > 0 && type === 'last') { + diff -= 7; + } else if (diff < 0 && type === 'next') { + diff += 7; + } + + date.setDate(date.getDate() + diff); + } + } + + function process(val) { + var splt = val.split(' '), // Todo: Reconcile this with regex using \s, taking into account browser issues with split and regexes + type = splt[0], + range = splt[1].substring(0, 3), + typeIsNumber = /\d+/.test(type), + ago = splt[2] === 'ago', + num = (type === 'last' ? -1 : 1) * (ago ? -1 : 1); + + if (typeIsNumber) { + num *= parseInt(type, 10); + } + + if (ranges.hasOwnProperty(range) && !splt[1].match(/^mon(day|\.)?$/i)) { + return date['set' + ranges[range]](date['get' + ranges[range]]() + num); + } + + if (range === 'wee') { + return date.setDate(date.getDate() + (num * 7)); + } + + if (type === 'next' || type === 'last') { + lastNext(type, range, num); + } else if (!typeIsNumber) { + return false; + } + + return true; + } + + times = '(years?|months?|weeks?|days?|hours?|minutes?|min|seconds?|sec' + + '|sunday|sun\\.?|monday|mon\\.?|tuesday|tue\\.?|wednesday|wed\\.?' + + '|thursday|thu\\.?|friday|fri\\.?|saturday|sat\\.?)'; + regex = '([+-]?\\d+\\s' + times + '|' + '(last|next)\\s' + times + ')(\\sago)?'; + + match = text.match(new RegExp(regex, 'gi')); + if (!match) { + return fail; + } + + for (i = 0, len = match.length; i < len; i++) { + if (!process(match[i])) { + return fail; + } + } + + // ECMAScript 5 only + // if (!match.every(process)) + // return false; + + return (date.getTime() / 1000); }; Twig.lib.is = function(type, obj) { @@ -1776,13 +2372,236 @@ var Twig = (function(Twig) { } return (isHalf ? value : Math.round(value)) / m; - } + }; + + Twig.lib.max = function max() { + // discuss at: http://phpjs.org/functions/max/ + // original by: Onno Marsman + // revised by: Onno Marsman + // improved by: Jack + // note: Long code cause we're aiming for maximum PHP compatibility + // example 1: max(1, 3, 5, 6, 7); + // returns 1: 7 + // example 2: max([2, 4, 5]); + // returns 2: 5 + // example 3: max(0, 'hello'); + // returns 3: 0 + // example 4: max('hello', 0); + // returns 4: 'hello' + // example 5: max(-1, 'hello'); + // returns 5: 'hello' + // example 6: max([2, 4, 8], [2, 5, 7]); + // returns 6: [2, 5, 7] + + var ar, retVal, i = 0, + n = 0, + argv = arguments, + argc = argv.length, + _obj2Array = function(obj) { + if (Object.prototype.toString.call(obj) === '[object Array]') { + return obj; + } else { + var ar = []; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + ar.push(obj[i]); + } + } + return ar; + } + }, //function _obj2Array + _compare = function(current, next) { + var i = 0, + n = 0, + tmp = 0, + nl = 0, + cl = 0; + + if (current === next) { + return 0; + } else if (typeof current === 'object') { + if (typeof next === 'object') { + current = _obj2Array(current); + next = _obj2Array(next); + cl = current.length; + nl = next.length; + if (nl > cl) { + return 1; + } else if (nl < cl) { + return -1; + } + for (i = 0, n = cl; i < n; ++i) { + tmp = _compare(current[i], next[i]); + if (tmp == 1) { + return 1; + } else if (tmp == -1) { + return -1; + } + } + return 0; + } + return -1; + } else if (typeof next === 'object') { + return 1; + } else if (isNaN(next) && !isNaN(current)) { + if (current == 0) { + return 0; + } + return (current < 0 ? 1 : -1); + } else if (isNaN(current) && !isNaN(next)) { + if (next == 0) { + return 0; + } + return (next > 0 ? 1 : -1); + } + + if (next == current) { + return 0; + } + return (next > current ? 1 : -1); + }; //function _compare + if (argc === 0) { + throw new Error('At least one value should be passed to max()'); + } else if (argc === 1) { + if (typeof argv[0] === 'object') { + ar = _obj2Array(argv[0]); + } else { + throw new Error('Wrong parameter count for max()'); + } + if (ar.length === 0) { + throw new Error('Array must contain at least one element for max()'); + } + } else { + ar = argv; + } + + retVal = ar[0]; + for (i = 1, n = ar.length; i < n; ++i) { + if (_compare(retVal, ar[i]) == 1) { + retVal = ar[i]; + } + } + + return retVal; + }; + + Twig.lib.min = function min() { + // discuss at: http://phpjs.org/functions/min/ + // original by: Onno Marsman + // revised by: Onno Marsman + // improved by: Jack + // note: Long code cause we're aiming for maximum PHP compatibility + // example 1: min(1, 3, 5, 6, 7); + // returns 1: 1 + // example 2: min([2, 4, 5]); + // returns 2: 2 + // example 3: min(0, 'hello'); + // returns 3: 0 + // example 4: min('hello', 0); + // returns 4: 'hello' + // example 5: min(-1, 'hello'); + // returns 5: -1 + // example 6: min([2, 4, 8], [2, 5, 7]); + // returns 6: [2, 4, 8] + + var ar, retVal, i = 0, + n = 0, + argv = arguments, + argc = argv.length, + _obj2Array = function(obj) { + if (Object.prototype.toString.call(obj) === '[object Array]') { + return obj; + } + var ar = []; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + ar.push(obj[i]); + } + } + return ar; + }, //function _obj2Array + _compare = function(current, next) { + var i = 0, + n = 0, + tmp = 0, + nl = 0, + cl = 0; + + if (current === next) { + return 0; + } else if (typeof current === 'object') { + if (typeof next === 'object') { + current = _obj2Array(current); + next = _obj2Array(next); + cl = current.length; + nl = next.length; + if (nl > cl) { + return 1; + } else if (nl < cl) { + return -1; + } + for (i = 0, n = cl; i < n; ++i) { + tmp = _compare(current[i], next[i]); + if (tmp == 1) { + return 1; + } else if (tmp == -1) { + return -1; + } + } + return 0; + } + return -1; + } else if (typeof next === 'object') { + return 1; + } else if (isNaN(next) && !isNaN(current)) { + if (current == 0) { + return 0; + } + return (current < 0 ? 1 : -1); + } else if (isNaN(current) && !isNaN(next)) { + if (next == 0) { + return 0; + } + return (next > 0 ? 1 : -1); + } + + if (next == current) { + return 0; + } + return (next > current ? 1 : -1); + }; //function _compare + + if (argc === 0) { + throw new Error('At least one value should be passed to min()'); + } else if (argc === 1) { + if (typeof argv[0] === 'object') { + ar = _obj2Array(argv[0]); + } else { + throw new Error('Wrong parameter count for min()'); + } + + if (ar.length === 0) { + throw new Error('Array must contain at least one element for min()'); + } + } else { + ar = argv; + } + + retVal = ar[0]; + + for (i = 1, n = ar.length; i < n; ++i) { + if (_compare(retVal, ar[i]) == -1) { + retVal = ar[i]; + } + } + + return retVal; + }; return Twig; })(Twig || { }); // Twig.js -// Copyright (c) 2011-2013 John Roepke // Available under the BSD 2-Clause License // https://github.com/justjohn/twig.js @@ -1812,6 +2631,7 @@ var Twig = (function (Twig) { endset: 'Twig.logic.type.endset', filter: 'Twig.logic.type.filter', endfilter: 'Twig.logic.type.endfilter', + shortblock: 'Twig.logic.type.shortblock', block: 'Twig.logic.type.block', endblock: 'Twig.logic.type.endblock', extends_: 'Twig.logic.type.extends', @@ -1822,7 +2642,9 @@ var Twig = (function (Twig) { macro: 'Twig.logic.type.macro', endmacro: 'Twig.logic.type.endmacro', import_: 'Twig.logic.type.import', - from: 'Twig.logic.type.from' + from: 'Twig.logic.type.from', + embed: 'Twig.logic.type.embed', + endembed: 'Twig.logic.type.endembed' }; @@ -1855,7 +2677,7 @@ var Twig = (function (Twig) { * Format: {% if expression %} */ type: Twig.logic.type.if_, - regex: /^if\s+([^\s].+)$/, + regex: /^if\s+([\s\S]+)$/, next: [ Twig.logic.type.else_, Twig.logic.type.elseif, @@ -2024,8 +2846,8 @@ var Twig = (function (Twig) { // Parse expression var result = Twig.expression.parse.apply(this, [token.expression, context]), output = [], - len, - index = 0, + len, + index = 0, keyset, that = this, conditional = token.conditional, @@ -2042,10 +2864,12 @@ var Twig = (function (Twig) { parent: context }; }, + // run once for each iteration of the loop loop = function(key, value) { - var inner_context = Twig.lib.copy(context); + var inner_context = Twig.ChildContext(context); inner_context[token.value_var] = value; + if (token.key_var) { inner_context[token.key_var] = key; } @@ -2059,22 +2883,32 @@ var Twig = (function (Twig) { output.push(Twig.parse.apply(that, [token.output, inner_context])); index += 1; } + + // Delete loop-related variables from the context + delete inner_context['loop']; + delete inner_context[token.value_var]; + delete inner_context[token.key_var]; + + // Merge in values that exist in context but have changed + // in inner_context. + Twig.merge(context, inner_context, true); }; - if (result instanceof Array) { + + if (Twig.lib.is('Array', result)) { len = result.length; Twig.forEach(result, function (value) { var key = index; loop(key, value); }); - } else if (result instanceof Object) { + } else if (Twig.lib.is('Object', result)) { if (result._keys !== undefined) { keyset = result._keys; } else { keyset = Object.keys(result); } - len = keyset.length; + len = keyset.length; Twig.forEach(keyset, function(key) { // Ignore the _keys property, it's internal to twig.js if (key === "_keys") return; @@ -2088,7 +2922,7 @@ var Twig = (function (Twig) { return { chain: continue_chain, - output: output.join("") + output: Twig.output.apply(this, [output]) }; } }, @@ -2110,7 +2944,7 @@ var Twig = (function (Twig) { * Format: {% set key = expression %} */ type: Twig.logic.type.set, - regex: /^set\s+([a-zA-Z0-9_,\s]+)\s*=\s*(.+)$/, + regex: /^set\s+([a-zA-Z0-9_,\s]+)\s*=\s*([\s\S]+)$/, next: [ ], open: true, compile: function (token) { @@ -2132,8 +2966,6 @@ var Twig = (function (Twig) { var value = Twig.expression.parse.apply(this, [token.expression, context]), key = token.key; - // set on both the global and local context - this.context[key] = value; context[key] = value; return { @@ -2254,23 +3086,44 @@ var Twig = (function (Twig) { return token; }, parse: function (token, context, chain) { - var block_output = "", - output = "", - hasParent = this.blocks[token.block] && this.blocks[token.block].indexOf(Twig.placeholders.parent) > -1; + var block_output, + output, + isImported = Twig.indexOf(this.importedBlocks, token.block) > -1, + hasParent = this.blocks[token.block] && Twig.indexOf(this.blocks[token.block], Twig.placeholders.parent) > -1; - // Don't override previous blocks + // Don't override previous blocks unless they're imported with "use" // Loops should be exempted as well. - if (this.blocks[token.block] === undefined || hasParent || context.loop) { - block_output = Twig.expression.parse.apply(this, [{ - type: Twig.expression.type.string, - value: Twig.parse.apply(this, [token.output, context]) - }, context]); + if (this.blocks[token.block] === undefined || isImported || hasParent || context.loop || token.overwrite) { + if (token.expression) { + // Short blocks have output as an expression on the open tag (no body) + block_output = Twig.expression.parse.apply(this, [{ + type: Twig.expression.type.string, + value: Twig.expression.parse.apply(this, [token.output, context]) + }, context]); + } else { + block_output = Twig.expression.parse.apply(this, [{ + type: Twig.expression.type.string, + value: Twig.parse.apply(this, [token.output, context]) + }, context]); + } + + if (isImported) { + // once the block is overridden, remove it from the list of imported blocks + this.importedBlocks.splice(this.importedBlocks.indexOf(token.block), 1); + } if (hasParent) { - this.blocks[token.block] = this.blocks[token.block].replace(Twig.placeholders.parent, block_output); + this.blocks[token.block] = Twig.Markup(this.blocks[token.block].replace(Twig.placeholders.parent, block_output)); } else { this.blocks[token.block] = block_output; } + + this.originalBlockTokens[token.block] = { + type: token.type, + block: token.block, + output: token.output, + overwrite: true + }; } // Check if a child block has been set from a template extending this one. @@ -2286,6 +3139,32 @@ var Twig = (function (Twig) { }; } }, + { + /** + * Block shorthand logic tokens. + * + * Format: {% block title expression %} + */ + type: Twig.logic.type.shortblock, + regex: /^block\s+([a-zA-Z0-9_]+)\s+(.+)$/, + next: [ ], + open: true, + compile: function (token) { + token.expression = token.match[2].trim(); + + token.output = Twig.expression.compile({ + type: Twig.expression.type.expression, + value: token.expression + }).stack; + + token.block = token.match[1].trim(); + delete token.match; + return token; + }, + parse: function (token, context, chain) { + return Twig.logic.handler[Twig.logic.type.block].parse.apply(this, arguments); + } + }, { /** * End block logic tokens. @@ -2335,7 +3214,7 @@ var Twig = (function (Twig) { /** * Block logic tokens. * - * Format: {% extends "template.twig" %} + * Format: {% use "template.twig" %} */ type: Twig.logic.type.use, regex: /^use\s+(.+)$/, @@ -2372,7 +3251,7 @@ var Twig = (function (Twig) { * Format: {% includes "template.twig" [with {some: 'values'} only] %} */ type: Twig.logic.type.include, - regex: /^include\s+(ignore missing\s+)?(.+?)\s*(?:with\s+(.+?))?\s*(only)?$/, + regex: /^include\s+(ignore missing\s+)?(.+?)\s*(?:with\s+([\S\s]+?))?\s*(only)?$/, next: [ ], open: true, compile: function (token) { @@ -2409,10 +3288,7 @@ var Twig = (function (Twig) { template; if (!token.only) { - for (i in context) { - if (context.hasOwnProperty(i)) - innerContext[i] = context[i]; - } + innerContext = Twig.ChildContext(context); } if (token.withStack !== undefined) { @@ -2426,8 +3302,12 @@ var Twig = (function (Twig) { var file = Twig.expression.parse.apply(this, [token.stack, innerContext]); - // Import file - template = this.importFile(file); + if (file instanceof Twig.Template) { + template = file; + } else { + // Import file + template = this.importFile(file); + } return { chain: chain, @@ -2474,14 +3354,14 @@ var Twig = (function (Twig) { * */ type: Twig.logic.type.macro, - regex: /^macro\s+([a-zA-Z0-9_]+)\s?\((([a-zA-Z0-9_]+(,\s?)?)*)\)$/, + regex: /^macro\s+([a-zA-Z0-9_]+)\s*\(\s*((?:[a-zA-Z0-9_]+(?:,\s*)?)*)\s*\)$/, next: [ Twig.logic.type.endmacro ], open: true, compile: function (token) { var macroName = token.match[1], - parameters = token.match[2].split(/[ ,]+/); + parameters = token.match[2].split(/[\s,]+/); //TODO: Clean up duplicate check for (var i=0; i -1; - } else { var el; for (el in b) { @@ -4089,7 +5066,6 @@ var Twig = (function (Twig) { })( Twig || { } ); // Twig.js -// Copyright (c) 2011-2013 John Roepke // Available under the BSD 2-Clause License // https://github.com/justjohn/twig.js @@ -4156,7 +5132,7 @@ var Twig = (function (Twig) { return value.reverse(); } else if (is("String", value)) { return value.split("").reverse().join(""); - } else if (value instanceof Object) { + } else if (is("Object", value)) { var keys = value._keys || Object.keys(value).reverse(); value._keys = keys; return value; @@ -4165,16 +5141,48 @@ var Twig = (function (Twig) { sort: function(value) { if (is("Array", value)) { return value.sort(); - } else if (value instanceof Object) { + } else if (is('Object', value)) { // Sorting objects isn't obvious since the order of - // returned keys isn't guaranteedin JavaScript. + // returned keys isn't guaranteed in JavaScript. // Because of this we use a "hidden" key called _keys to // store the keys in the order we want to return them. delete value._keys; var keys = Object.keys(value), sorted_keys = keys.sort(function(a, b) { - return value[a] > value[b]; + var a1, a2; + + // if a and b are comparable, we're fine :-) + if((value[a] > value[b]) == !(value[a] <= value[b])) { + return value[a] > value[b] ? 1 : + value[a] < value[b] ? -1 : + 0; + } + // if a and b can be parsed as numbers, we can compare + // their numeric value + else if(!isNaN(a1 = parseFloat(value[a])) && + !isNaN(b1 = parseFloat(value[b]))) { + return a1 > b1 ? 1 : + a1 < b1 ? -1 : + 0; + } + // if one of the values is a string, we convert the + // other value to string as well + else if(typeof value[a] == 'string') { + return value[a] > value[b].toString() ? 1 : + value[a] < value[b].toString() ? -1 : + 0; + } + else if(typeof value[b] == 'string') { + return value[a].toString() > value[b] ? 1 : + value[a].toString() < value[b] ? -1 : + 0; + } + // everything failed - return 'null' as sign, that + // the values are not comparable + else { + return null; + } }); value._keys = sorted_keys; return value; @@ -4201,7 +5209,9 @@ var Twig = (function (Twig) { return; } - return encodeURIComponent(value); + var result = encodeURIComponent(value); + result = result.replace("'", "%27"); + return result; }, join: function(value, params) { if (value === undefined || value === null){ @@ -4215,7 +5225,7 @@ var Twig = (function (Twig) { if (params && params[0]) { join_str = params[0]; } - if (value instanceof Array) { + if (is("Array", value)) { output = value; } else { keyset = value._keys || Object.keys(value); @@ -4229,23 +5239,45 @@ var Twig = (function (Twig) { return output.join(join_str); }, "default": function(value, params) { - if (params === undefined || params.length !== 1) { + if (params !== undefined && params.length > 1) { throw new Twig.Error("default filter expects one argument"); } if (value === undefined || value === null || value === '' ) { + if (params === undefined) { + return ''; + } + return params[0]; } else { return value; } }, json_encode: function(value) { - if (value && value.hasOwnProperty( "_keys" ) ) { - delete value._keys; - } if(value === undefined || value === null) { return "null"; } - return JSON.stringify(value); + else if ((typeof value == 'object') && (is("Array", value))) { + output = []; + + Twig.forEach(value, function(v) { + output.push(Twig.filters.json_encode(v)); + }); + + return "[" + output.join(",") + "]"; + } + else if (typeof value == 'object') { + var keyset = value._keys || Object.keys(value), + output = []; + + Twig.forEach(keyset, function(key) { + output.push(JSON.stringify(key) + ":" + Twig.filters.json_encode(value[key])); + }); + + return "{" + output.join(",") + "}"; + } + else { + return JSON.stringify(value); + } }, merge: function(value, params) { var obj = [], @@ -4253,21 +5285,21 @@ var Twig = (function (Twig) { keyset = []; // Check to see if all the objects being merged are arrays - if (!(value instanceof Array)) { + if (!is("Array", value)) { // Create obj as an Object obj = { }; } else { Twig.forEach(params, function(param) { - if (!(param instanceof Array)) { + if (!is("Array", param)) { obj = { }; } }); } - if (!(obj instanceof Array)) { + if (!is("Array", obj)) { obj._keys = []; } - if (value instanceof Array) { + if (is("Array", value)) { Twig.forEach(value, function(val) { if (obj._keys) obj._keys.push(arr_index); obj[arr_index] = val; @@ -4295,7 +5327,7 @@ var Twig = (function (Twig) { // mixin the merge arrays Twig.forEach(params, function(param) { - if (param instanceof Array) { + if (is("Array", param)) { Twig.forEach(param, function(val) { if (obj._keys) obj._keys.push(arr_index); obj[arr_index] = val; @@ -4321,12 +5353,9 @@ var Twig = (function (Twig) { return obj; }, date: function(value, params) { - if (value === undefined||value === null){ - return; - } - var date = Twig.functions.date(value); - return Twig.lib.formatDate(date, params[0]); + var format = params && params.length ? params[0] : 'F j, Y H:i'; + return Twig.lib.formatDate(date, format); }, date_modify: function(value, params) { @@ -4383,20 +5412,92 @@ var Twig = (function (Twig) { return Twig.lib.strip_tags(value); }, - escape: function(value) { + escape: function(value, params) { if (value === undefined|| value === null){ return; } - return value.toString().replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); + + var strategy = "html"; + if(params && params.length && params[0] !== true) + strategy = params[0]; + + if(strategy == "html") { + var raw_value = value.toString().replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + return Twig.Markup(raw_value, 'html'); + } else if(strategy == "js") { + var raw_value = value.toString(); + var result = ""; + + for(var i = 0; i < raw_value.length; i++) { + if(raw_value[i].match(/^[a-zA-Z0-9,\._]$/)) + result += raw_value[i]; + else { + var char_code = raw_value.charCodeAt(i); + + if(char_code < 0x80) + result += "\\x" + char_code.toString(16).toUpperCase(); + else + result += Twig.lib.sprintf("\\u%04s", char_code.toString(16).toUpperCase()); + } + } + + return Twig.Markup(result, 'js'); + } else if(strategy == "css") { + var raw_value = value.toString(); + var result = ""; + + for(var i = 0; i < raw_value.length; i++) { + if(raw_value[i].match(/^[a-zA-Z0-9]$/)) + result += raw_value[i]; + else { + var char_code = raw_value.charCodeAt(i); + result += "\\" + char_code.toString(16).toUpperCase() + " "; + } + } + + return Twig.Markup(result, 'css'); + } else if(strategy == "url") { + var result = Twig.filters.url_encode(value); + return Twig.Markup(result, 'url'); + } else if(strategy == "html_attr") { + var raw_value = value.toString(); + var result = ""; + + for(var i = 0; i < raw_value.length; i++) { + if(raw_value[i].match(/^[a-zA-Z0-9,\.\-_]$/)) + result += raw_value[i]; + else if(raw_value[i].match(/^[&<>"]$/)) + result += raw_value[i].replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + else { + var char_code = raw_value.charCodeAt(i); + + // The following replaces characters undefined in HTML with + // the hex entity for the Unicode replacement character. + if(char_code <= 0x1f && char_code != 0x09 && char_code != 0x0a && char_code != 0x0d) + result += "�"; + else if(char_code < 0x80) + result += Twig.lib.sprintf("&#x%02s;", char_code.toString(16).toUpperCase()); + else + result += Twig.lib.sprintf("&#x%04s;", char_code.toString(16).toUpperCase()); + } + } + + return Twig.Markup(result, 'html_attr'); + } else { + throw new Twig.Error("escape strategy unsupported"); + } }, /* Alias of escape */ - "e": function(value) { - return Twig.filters.escape(value); + "e": function(value, params) { + return Twig.filters.escape(value, params); }, nl2br: function(value) { @@ -4411,7 +5512,9 @@ var Twig = (function (Twig) { .replace(/\r/g, br) .replace(/\n/g, br); - return Twig.lib.replaceAll(value, linebreak_tag, "\n"); + value = Twig.lib.replaceAll(value, linebreak_tag, "\n"); + + return Twig.Markup(value); }, /** @@ -4470,6 +5573,39 @@ var Twig = (function (Twig) { return whitespace.indexOf(str.charAt(0)) === -1 ? str : ''; }, + truncate: function (value, params) { + var length = 30, + preserve = false, + separator = '...'; + + value = value + ''; + if (params) { + if (params[0]) { + length = params[0]; + } + if (params[1]) { + preserve = params[1]; + } + if (params[2]) { + separator = params[2]; + } + } + + if (value.length > length) { + + if (preserve) { + length = value.indexOf(' ', length); + if (length === -1) { + return value; + } + } + + value = value.substr(0, length) + separator; + } + + return value; + }, + slice: function(value, params) { if (value === undefined || value === null) { return; @@ -4507,9 +5643,9 @@ var Twig = (function (Twig) { }, first: function(value) { - if (value instanceof Array) { + if (is("Array", value)) { return value[0]; - } else if (value instanceof Object) { + } else if (is("Object", value)) { if ('_keys' in value) { return value[value._keys[0]]; } @@ -4595,8 +5731,7 @@ var Twig = (function (Twig) { return value[value.length - 1]; }, raw: function(value) { - //Raw filter shim - return value; + return Twig.Markup(value); }, batch: function(items, params) { var size = params.shift(), @@ -4669,7 +5804,6 @@ var Twig = (function (Twig) { })(Twig || { }); // Twig.js -// Copyright (c) 2011-2013 John Roepke // 2012 Hadrien Lanneau // Available under the BSD 2-Clause License // https://github.com/justjohn/twig.js @@ -4678,6 +5812,11 @@ var Twig = (function (Twig) { // // This file handles parsing filters. var Twig = (function (Twig) { + /** + * @constant + * @type {string} + */ + var TEMPLATE_NOT_FOUND_MESSAGE = 'Template "{name}" is not defined.'; // Determine object type function is(type, obj) { @@ -4739,19 +5878,19 @@ var Twig = (function (Twig) { }, dump: function() { var EOL = '\n', - indentChar = ' ', - indentTimes = 0, - out = '', - args = Array.prototype.slice.call(arguments), - indent = function(times) { - var ind = ''; + indentChar = ' ', + indentTimes = 0, + out = '', + args = Array.prototype.slice.call(arguments), + indent = function(times) { + var ind = ''; while (times > 0) { times--; ind += indentChar; } return ind; }, - displayVar = function(variable) { + displayVar = function(variable) { out += indent(indentTimes); if (typeof(variable) === 'object') { dumpVar(variable); @@ -4765,41 +5904,41 @@ var Twig = (function (Twig) { out += 'bool(' + variable + ')' + EOL; } }, - dumpVar = function(variable) { - var i; - if (variable === null) { - out += 'NULL' + EOL; - } else if (variable === undefined) { - out += 'undefined' + EOL; - } else if (typeof variable === 'object') { - out += indent(indentTimes) + typeof(variable); - indentTimes++; - out += '(' + (function(obj) { - var size = 0, key; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - size++; - } - } - return size; - })(variable) + ') {' + EOL; - for (i in variable) { - out += indent(indentTimes) + '[' + i + ']=> ' + EOL; - displayVar(variable[i]); - } - indentTimes--; - out += indent(indentTimes) + '}' + EOL; - } else { - displayVar(variable); - } - }; - - // handle no argument case by dumping the entire render context - if (args.length == 0) args.push(this.context); - - Twig.forEach(args, function(variable) { - dumpVar(variable); - }); + dumpVar = function(variable) { + var i; + if (variable === null) { + out += 'NULL' + EOL; + } else if (variable === undefined) { + out += 'undefined' + EOL; + } else if (typeof variable === 'object') { + out += indent(indentTimes) + typeof(variable); + indentTimes++; + out += '(' + (function(obj) { + var size = 0, key; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + size++; + } + } + return size; + })(variable) + ') {' + EOL; + for (i in variable) { + out += indent(indentTimes) + '[' + i + ']=> ' + EOL; + displayVar(variable[i]); + } + indentTimes--; + out += indent(indentTimes) + '}' + EOL; + } else { + displayVar(variable); + } + }; + + // handle no argument case by dumping the entire render context + if (args.length == 0) args.push(this.context); + + Twig.forEach(args, function(variable) { + dumpVar(variable); + }); return out; }, @@ -4810,7 +5949,12 @@ var Twig = (function (Twig) { } else if (Twig.lib.is("Date", date)) { dateObj = date; } else if (Twig.lib.is("String", date)) { - dateObj = new Date(Twig.lib.strtotime(date) * 1000); + if (date.match(/^[0-9]+$/)) { + dateObj = new Date(date * 1000); + } + else { + dateObj = new Date(Twig.lib.strtotime(date) * 1000); + } } else if (Twig.lib.is("Number", date)) { // timestamp dateObj = new Date(date * 1000); @@ -4820,14 +5964,18 @@ var Twig = (function (Twig) { return dateObj; }, block: function(block) { - return this.blocks[block]; + if (this.originalBlockTokens[block]) { + return Twig.logic.parse.apply(this, [this.originalBlockTokens[block], this.context]).output; + } else { + return this.blocks[block]; + } }, parent: function() { // Add a placeholder return Twig.placeholders.parent; }, attribute: function(object, method, params) { - if (object instanceof Object) { + if (Twig.lib.is('Object', object)) { if (object.hasOwnProperty(method)) { if (typeof object[method] === "function") { return object[method].apply(undefined, params); @@ -4839,6 +5987,130 @@ var Twig = (function (Twig) { } // Array will return element 0-index return object[method] || undefined; + }, + max: function(values) { + if(Twig.lib.is("Object", values)) { + delete values["_keys"]; + return Twig.lib.max(values); + } + + return Twig.lib.max.apply(null, arguments); + }, + min: function(values) { + if(Twig.lib.is("Object", values)) { + delete values["_keys"]; + return Twig.lib.min(values); + } + + return Twig.lib.min.apply(null, arguments); + }, + template_from_string: function(template) { + if (template === undefined) { + template = ''; + } + return Twig.Templates.parsers.twig({ + options: this.options, + data: template + }); + }, + random: function(value) { + var LIMIT_INT31 = 0x80000000; + + function getRandomNumber(n) { + var random = Math.floor(Math.random() * LIMIT_INT31); + var limits = [0, n]; + var min = Math.min.apply(null, limits), + max = Math.max.apply(null, limits); + return min + Math.floor((max - min + 1) * random / LIMIT_INT31); + } + + if(Twig.lib.is("Number", value)) { + return getRandomNumber(value); + } + + if(Twig.lib.is("String", value)) { + return value.charAt(getRandomNumber(value.length-1)); + } + + if(Twig.lib.is("Array", value)) { + return value[getRandomNumber(value.length-1)]; + } + + if(Twig.lib.is("Object", value)) { + var keys = Object.keys(value); + return value[keys[getRandomNumber(keys.length-1)]]; + } + + return getRandomNumber(LIMIT_INT31-1); + }, + + /** + * Returns the content of a template without rendering it + * @param {string} name + * @param {boolean} [ignore_missing=false] + * @returns {string} + */ + source: function(name, ignore_missing) { + var templateSource; + var templateFound = false; + var isNodeEnvironment = typeof module !== 'undefined' && typeof module.exports !== 'undefined' && typeof window === 'undefined'; + var loader; + var path; + + //if we are running in a node.js environment, set the loader to 'fs' and ensure the + // path is relative to the CWD of the running script + //else, set the loader to 'ajax' and set the path to the value of name + if (isNodeEnvironment) { + loader = 'fs'; + path = __dirname + '/' + name; + } else { + loader = 'ajax'; + path = name; + } + + //build the params object + var params = { + id: name, + path: path, + method: loader, + parser: 'source', + async: false, + fetchTemplateSource: true + }; + + //default ignore_missing to false + if (typeof ignore_missing === 'undefined') { + ignore_missing = false; + } + + //try to load the remote template + // + //on exception, log it + try { + templateSource = Twig.Templates.loadRemote(name, params); + + //if the template is undefined or null, set the template to an empty string and do NOT flip the + // boolean indicating we found the template + // + //else, all is good! flip the boolean indicating we found the template + if (typeof templateSource === 'undefined' || templateSource === null) { + templateSource = ''; + } else { + templateFound = true; + } + } catch (e) { + Twig.log.debug('Twig.functions.source: ', 'Problem loading template ', e); + } + + //if the template was NOT found AND we are not ignoring missing templates, return the same message + // that is returned by the PHP implementation of the twig source() function + // + //else, return the template source + if (!templateFound && !ignore_missing) { + return TEMPLATE_NOT_FOUND_MESSAGE.replace('{name}', name); + } else { + return templateSource; + } } }; @@ -4857,7 +6129,119 @@ var Twig = (function (Twig) { })(Twig || { }); // Twig.js -// Copyright (c) 2011-2013 John Roepke +// Available under the BSD 2-Clause License +// https://github.com/justjohn/twig.js + +// ## twig.path.js +// +// This file handles path parsing +var Twig = (function (Twig) { + "use strict"; + + /** + * Namespace for path handling. + */ + Twig.path = {}; + + /** + * Generate the canonical version of a url based on the given base path and file path and in + * the previously registered namespaces. + * + * @param {string} template The Twig Template + * @param {string} file The file path, may be relative and may contain namespaces. + * + * @return {string} The canonical version of the path + */ + Twig.path.parsePath = function(template, file) { + var namespaces = null, + file = file || ""; + + if (typeof template === 'object' && typeof template.options === 'object') { + namespaces = template.options.namespaces; + } + + if (typeof namespaces === 'object' && (file.indexOf('::') > 0) || file.indexOf('@') >= 0){ + for (var k in namespaces){ + if (namespaces.hasOwnProperty(k)) { + file = file.replace(k + '::', namespaces[k]); + file = file.replace('@' + k, namespaces[k]); + } + } + + return file; + } + + return Twig.path.relativePath(template, file); + }; + + /** + * Generate the relative canonical version of a url based on the given base path and file path. + * + * @param {Twig.Template} template The Twig.Template. + * @param {string} file The file path, relative to the base path. + * + * @return {string} The canonical version of the path. + */ + Twig.path.relativePath = function(template, file) { + var base, + base_path, + sep_chr = "/", + new_path = [], + file = file || "", + val; + + if (template.url) { + if (typeof template.base !== 'undefined') { + base = template.base + ((template.base.charAt(template.base.length-1) === '/') ? '' : '/'); + } else { + base = template.url; + } + } else if (template.path) { + // Get the system-specific path separator + var path = require("path"), + sep = path.sep || sep_chr, + relative = new RegExp("^\\.{1,2}" + sep.replace("\\", "\\\\")); + file = file.replace(/\//g, sep); + + if (template.base !== undefined && file.match(relative) == null) { + file = file.replace(template.base, ''); + base = template.base + sep; + } else { + base = path.normalize(template.path); + } + + base = base.replace(sep+sep, sep); + sep_chr = sep; + } else if ((template.name || template.id) && template.method && template.method !== 'fs' && template.method !== 'ajax') { + // Custom registered loader + base = template.base || template.name || template.id; + } else { + throw new Twig.Error("Cannot extend an inline template."); + } + + base_path = base.split(sep_chr); + + // Remove file from url + base_path.pop(); + base_path = base_path.concat(file.split(sep_chr)); + + while (base_path.length > 0) { + val = base_path.shift(); + if (val == ".") { + // Ignore + } else if (val == ".." && new_path.length > 0 && new_path[new_path.length-1] != "..") { + new_path.pop(); + } else { + new_path.push(val); + } + } + + return new_path.join(sep_chr); + }; + + return Twig; +}) (Twig || { }); +// Twig.js // Available under the BSD 2-Clause License // https://github.com/justjohn/twig.js @@ -4899,6 +6283,9 @@ var Twig = (function (Twig) { }, sameas: function(value, params) { return value === params[0]; + }, + iterable: function(value) { + return value && (Twig.lib.is("Array", value) || Twig.lib.is("Object", value)); } /* constant ? @@ -4919,7 +6306,6 @@ var Twig = (function (Twig) { return Twig; })( Twig || { } ); // Twig.js -// Copyright (c) 2011-2013 John Roepke // Available under the BSD 2-Clause License // https://github.com/justjohn/twig.js @@ -4944,12 +6330,15 @@ var Twig = (function (Twig) { 'use strict'; var id = params.id, options = { - strict_variables: params.strict_variables || false, + strictVariables: params.strictVariables || false, + // TODO: turn autoscape on in the next major version + autoescape: params.autoescape != null && params.autoescape || false, allowInlineIncludes: params.allowInlineIncludes || false, - rethrow: params.rethrow || false + rethrow: params.rethrow || false, + namespaces: params.namespaces }; - if (id) { + if (Twig.cache && id) { Twig.validateId(id); } @@ -4961,8 +6350,9 @@ var Twig = (function (Twig) { } if (params.data !== undefined) { - return new Twig.Template({ + return Twig.Templates.parsers.twig({ data: params.data, + path: params.hasOwnProperty('path') ? params.path : undefined, module: params.module, id: id, options: options @@ -4974,10 +6364,27 @@ var Twig = (function (Twig) { } return Twig.Templates.load(params.ref); + } else if (params.method !== undefined) { + if (!Twig.Templates.isRegisteredLoader(params.method)) { + throw new Twig.Error('Loader for "' + params.method + '" is not defined.'); + } + return Twig.Templates.loadRemote(params.name || params.href || params.path || id || undefined, { + id: id, + method: params.method, + parser: params.parser || 'twig', + base: params.base, + module: params.module, + precompiled: params.precompiled, + async: params.async, + options: options + + }, params.load, params.error); + } else if (params.href !== undefined) { return Twig.Templates.loadRemote(params.href, { id: id, method: 'ajax', + parser: params.parser || 'twig', base: params.base, module: params.module, precompiled: params.precompiled, @@ -4990,6 +6397,7 @@ var Twig = (function (Twig) { return Twig.Templates.loadRemote(params.path, { id: id, method: 'fs', + parser: params.parser || 'twig', base: params.base, module: params.module, precompiled: params.precompiled, @@ -5059,32 +6467,37 @@ var Twig = (function (Twig) { * @param {string} path The location of the template file on disk. * @param {Object|Function} The options or callback. * @param {Function} fn callback. + * + * @throws Twig.Error */ - Twig.exports.renderFile = function(path, options, fn) { // handle callback in options - if ('function' == typeof options) { + if (typeof options === 'function') { fn = options; options = {}; } options = options || {}; + var settings = options.settings || {}; + var params = { - path: path, - base: options.settings['views'], - load: function(template) { - // render and return template - fn(null, template.render(options)); - } - }; + path: path, + base: settings.views, + load: function(template) { + // render and return template + fn(null, template.render(options)); + } + }; // mixin any options provided to the express app. - var view_options = options.settings['twig options']; + var view_options = settings['twig options']; if (view_options) { - for (var option in view_options) if (view_options.hasOwnProperty(option)) { - params[option] = view_options[option]; + for (var option in view_options) { + if (view_options.hasOwnProperty(option)) { + params[option] = view_options[option]; + } } } @@ -5103,13 +6516,15 @@ var Twig = (function (Twig) { */ Twig.exports.cache = function(cache) { Twig.cache = cache; - } + }; + + //We need to export the path module so we can effectively test it + Twig.exports.path = Twig.path; return Twig; }) (Twig || { }); // Twig.js -// Copyright (c) 2011-2013 John Roepke // Available under the BSD 2-Clause License // https://github.com/justjohn/twig.js @@ -5165,7 +6580,6 @@ var Twig = (function (Twig) { return Twig; })(Twig || {}); // Twig.js -// Copyright (c) 2011-2013 John Roepke // Available under the BSD 2-Clause License // https://github.com/justjohn/twig.js diff --git a/demos/node_express/public/vendor/zepto.history.js b/demos/node_express/public/vendor/zepto.history.js index c1e0baf9..1f416039 100644 --- a/demos/node_express/public/vendor/zepto.history.js +++ b/demos/node_express/public/vendor/zepto.history.js @@ -1 +1,480 @@ -window.JSON||(window.JSON={}),function(){function f(a){return a<10?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c")&&c[0]);return a>4?a:!1}();return a},m.isInternetExplorer=function(){var a=m.isInternetExplorer.cached=typeof m.isInternetExplorer.cached!="undefined"?m.isInternetExplorer.cached:Boolean(m.getInternetExplorerMajorVersion());return a},m.emulated={pushState:!Boolean(a.history&&a.history.pushState&&a.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)),hashChange:Boolean(!("onhashchange"in a||"onhashchange"in d)||m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8)},m.enabled=!m.emulated.pushState,m.bugs={setHash:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),safariPoll:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),ieDoubleCheck:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<7)},m.isEmptyObject=function(a){for(var b in a)return!1;return!0},m.cloneObject=function(a){var b,c;return a?(b=k.stringify(a),c=k.parse(b)):c={},c},m.getRootUrl=function(){var a=d.location.protocol+"//"+(d.location.hostname||d.location.host);if(d.location.port||!1)a+=":"+d.location.port;return a+="/",a},m.getBaseHref=function(){var a=d.getElementsByTagName("base"),b=null,c="";return a.length===1&&(b=a[0],c=b.href.replace(/[^\/]+$/,"")),c=c.replace(/\/+$/,""),c&&(c+="/"),c},m.getBaseUrl=function(){var a=m.getBaseHref()||m.getBasePageUrl()||m.getRootUrl();return a},m.getPageUrl=function(){var a=m.getState(!1,!1),b=(a||{}).url||d.location.href,c;return c=b.replace(/\/+$/,"").replace(/[^\/]+$/,function(a,b,c){return/\./.test(a)?a:a+"/"}),c},m.getBasePageUrl=function(){var a=d.location.href.replace(/[#\?].*/,"").replace(/[^\/]+$/,function(a,b,c){return/[^\/]$/.test(a)?"":a}).replace(/\/+$/,"")+"/";return a},m.getFullUrl=function(a,b){var c=a,d=a.substring(0,1);return b=typeof b=="undefined"?!0:b,/[a-z]+\:\/\//.test(a)||(d==="/"?c=m.getRootUrl()+a.replace(/^\/+/,""):d==="#"?c=m.getPageUrl().replace(/#.*/,"")+a:d==="?"?c=m.getPageUrl().replace(/[\?#].*/,"")+a:b?c=m.getBaseUrl()+a.replace(/^(\.\/)+/,""):c=m.getBasePageUrl()+a.replace(/^(\.\/)+/,"")),c.replace(/\#$/,"")},m.getShortUrl=function(a){var b=a,c=m.getBaseUrl(),d=m.getRootUrl();return m.emulated.pushState&&(b=b.replace(c,"")),b=b.replace(d,"/"),m.isTraditionalAnchor(b)&&(b="./"+b),b=b.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),b},m.store={},m.idToState=m.idToState||{},m.stateToId=m.stateToId||{},m.urlToId=m.urlToId||{},m.storedStates=m.storedStates||[],m.savedStates=m.savedStates||[],m.normalizeStore=function(){m.store.idToState=m.store.idToState||{},m.store.urlToId=m.store.urlToId||{},m.store.stateToId=m.store.stateToId||{}},m.getState=function(a,b){typeof a=="undefined"&&(a=!0),typeof b=="undefined"&&(b=!0);var c=m.getLastSavedState();return!c&&b&&(c=m.createStateObject()),a&&(c=m.cloneObject(c),c.url=c.cleanUrl||c.url),c},m.getIdByState=function(a){var b=m.extractId(a.url),c;if(!b){c=m.getStateString(a);if(typeof m.stateToId[c]!="undefined")b=m.stateToId[c];else if(typeof m.store.stateToId[c]!="undefined")b=m.store.stateToId[c];else{for(;;){b=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof m.idToState[b]=="undefined"&&typeof m.store.idToState[b]=="undefined")break}m.stateToId[c]=b,m.idToState[b]=a}}return b},m.normalizeState=function(a){var b,c;if(!a||typeof a!="object")a={};if(typeof a.normalized!="undefined")return a;if(!a.data||typeof a.data!="object")a.data={};b={},b.normalized=!0,b.title=a.title||"",b.url=m.getFullUrl(m.unescapeString(a.url||d.location.href)),b.hash=m.getShortUrl(b.url),b.data=m.cloneObject(a.data),b.id=m.getIdByState(b),b.cleanUrl=b.url.replace(/\??\&_suid.*/,""),b.url=b.cleanUrl,c=!m.isEmptyObject(b.data);if(b.title||c)b.hash=m.getShortUrl(b.url).replace(/\??\&_suid.*/,""),/\?/.test(b.hash)||(b.hash+="?"),b.hash+="&_suid="+b.id;return b.hashedUrl=m.getFullUrl(b.hash),(m.emulated.pushState||m.bugs.safariPoll)&&m.hasUrlDuplicate(b)&&(b.url=b.hashedUrl),b},m.createStateObject=function(a,b,c){var d={data:a,title:b,url:c};return d=m.normalizeState(d),d},m.getStateById=function(a){a=String(a);var c=m.idToState[a]||m.store.idToState[a]||b;return c},m.getStateString=function(a){var b,c,d;return b=m.normalizeState(a),c={data:b.data,title:a.title,url:a.url},d=k.stringify(c),d},m.getStateId=function(a){var b,c;return b=m.normalizeState(a),c=b.id,c},m.getHashByState=function(a){var b,c;return b=m.normalizeState(a),c=b.hash,c},m.extractId=function(a){var b,c,d;return c=/(.*)\&_suid=([0-9]+)$/.exec(a),d=c?c[1]||a:a,b=c?String(c[2]||""):"",b||!1},m.isTraditionalAnchor=function(a){var b=!/[\/\?\.]/.test(a);return b},m.extractState=function(a,b){var c=null,d,e;return b=b||!1,d=m.extractId(a),d&&(c=m.getStateById(d)),c||(e=m.getFullUrl(a),d=m.getIdByUrl(e)||!1,d&&(c=m.getStateById(d)),!c&&b&&!m.isTraditionalAnchor(a)&&(c=m.createStateObject(null,null,e))),c},m.getIdByUrl=function(a){var c=m.urlToId[a]||m.store.urlToId[a]||b;return c},m.getLastSavedState=function(){return m.savedStates[m.savedStates.length-1]||b},m.getLastStoredState=function(){return m.storedStates[m.storedStates.length-1]||b},m.hasUrlDuplicate=function(a){var b=!1,c;return c=m.extractState(a.url),b=c&&c.id!==a.id,b},m.storeState=function(a){return m.urlToId[a.url]=a.id,m.storedStates.push(m.cloneObject(a)),a},m.isLastSavedState=function(a){var b=!1,c,d,e;return m.savedStates.length&&(c=a.id,d=m.getLastSavedState(),e=d.id,b=c===e),b},m.saveState=function(a){return m.isLastSavedState(a)?!1:(m.savedStates.push(m.cloneObject(a)),!0)},m.getStateByIndex=function(a){var b=null;return typeof a=="undefined"?b=m.savedStates[m.savedStates.length-1]:a<0?b=m.savedStates[m.savedStates.length+a]:b=m.savedStates[a],b},m.getHash=function(){var a=m.unescapeHash(d.location.hash);return a},m.unescapeString=function(b){var c=b,d;for(;;){d=a.unescape(c);if(d===c)break;c=d}return c},m.unescapeHash=function(a){var b=m.normalizeHash(a);return b=m.unescapeString(b),b},m.normalizeHash=function(a){var b=a.replace(/[^#]*#/,"").replace(/#.*/,"");return b},m.setHash=function(a,b){var c,e,f;return b!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.setHash,args:arguments,queue:b}),!1):(c=m.escapeHash(a),m.busy(!0),e=m.extractState(a,!0),e&&!m.emulated.pushState?m.pushState(e.data,e.title,e.url,!1):d.location.hash!==c&&(m.bugs.setHash?(f=m.getPageUrl(),m.pushState(null,null,f+"#"+c,!1)):d.location.hash=c),m)},m.escapeHash=function(b){var c=m.normalizeHash(b);return c=a.escape(c),m.bugs.hashEscape||(c=c.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),c},m.getHashByUrl=function(a){var b=String(a).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return b=m.unescapeHash(b),b},m.setTitle=function(a){var b=a.title,c;b||(c=m.getStateByIndex(0),c&&c.url===a.url&&(b=c.title||m.options.initialTitle));try{d.getElementsByTagName("title")[0].innerHTML=b.replace("<","<").replace(">",">").replace(" & "," & ")}catch(e){}return d.title=b,m},m.queues=[],m.busy=function(a){typeof a!="undefined"?m.busy.flag=a:typeof m.busy.flag=="undefined"&&(m.busy.flag=!1);if(!m.busy.flag){h(m.busy.timeout);var b=function(){var a,c,d;if(m.busy.flag)return;for(a=m.queues.length-1;a>=0;--a){c=m.queues[a];if(c.length===0)continue;d=c.shift(),m.fireQueueItem(d),m.busy.timeout=g(b,m.options.busyDelay)}};m.busy.timeout=g(b,m.options.busyDelay)}return m.busy.flag},m.busy.flag=!1,m.fireQueueItem=function(a){return a.callback.apply(a.scope||m,a.args||[])},m.pushQueue=function(a){return m.queues[a.queue||0]=m.queues[a.queue||0]||[],m.queues[a.queue||0].push(a),m},m.queue=function(a,b){return typeof a=="function"&&(a={callback:a}),typeof b!="undefined"&&(a.queue=b),m.busy()?m.pushQueue(a):m.fireQueueItem(a),m},m.clearQueue=function(){return m.busy.flag=!1,m.queues=[],m},m.stateChanged=!1,m.doubleChecker=!1,m.doubleCheckComplete=function(){return m.stateChanged=!0,m.doubleCheckClear(),m},m.doubleCheckClear=function(){return m.doubleChecker&&(h(m.doubleChecker),m.doubleChecker=!1),m},m.doubleCheck=function(a){return m.stateChanged=!1,m.doubleCheckClear(),m.bugs.ieDoubleCheck&&(m.doubleChecker=g(function(){return m.doubleCheckClear(),m.stateChanged||a(),!0},m.options.doubleCheckInterval)),m},m.safariStatePoll=function(){var b=m.extractState(d.location.href),c;if(!m.isLastSavedState(b))c=b;else return;return c||(c=m.createStateObject()),m.Adapter.trigger(a,"popstate"),m},m.back=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.back,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.back(!1)}),n.go(-1),!0)},m.forward=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.forward,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.forward(!1)}),n.go(1),!0)},m.go=function(a,b){var c;if(a>0)for(c=1;c<=a;++c)m.forward(b);else{if(!(a<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(c=-1;c>=a;--c)m.back(b)}return m};if(m.emulated.pushState){var o=function(){};m.pushState=m.pushState||o,m.replaceState=m.replaceState||o}else m.onPopState=function(b,c){var e=!1,f=!1,g,h;return m.doubleCheckComplete(),g=m.getHash(),g?(h=m.extractState(g||d.location.href,!0),h?m.replaceState(h.data,h.title,h.url,!1):(m.Adapter.trigger(a,"anchorchange"),m.busy(!1)),m.expectedStateId=!1,!1):(e=m.Adapter.extractEventData("state",b,c)||!1,e?f=m.getStateById(e):m.expectedStateId?f=m.getStateById(m.expectedStateId):f=m.extractState(d.location.href),f||(f=m.createStateObject(null,null,d.location.href)),m.expectedStateId=!1,m.isLastSavedState(f)?(m.busy(!1),!1):(m.storeState(f),m.saveState(f),m.setTitle(f),m.Adapter.trigger(a,"statechange"),m.busy(!1),!0))},m.Adapter.bind(a,"popstate",m.onPopState),m.pushState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.pushState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.pushState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0},m.replaceState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.replaceState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.replaceState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0};if(f){try{m.store=k.parse(f.getItem("History.store"))||{}}catch(p){m.store={}}m.normalizeStore()}else m.store={},m.normalizeStore();m.Adapter.bind(a,"beforeunload",m.clearAllIntervals),m.Adapter.bind(a,"unload",m.clearAllIntervals),m.saveState(m.storeState(m.extractState(d.location.href,!0))),f&&(m.onUnload=function(){var a,b;try{a=k.parse(f.getItem("History.store"))||{}}catch(c){a={}}a.idToState=a.idToState||{},a.urlToId=a.urlToId||{},a.stateToId=a.stateToId||{};for(b in m.idToState){if(!m.idToState.hasOwnProperty(b))continue;a.idToState[b]=m.idToState[b]}for(b in m.urlToId){if(!m.urlToId.hasOwnProperty(b))continue;a.urlToId[b]=m.urlToId[b]}for(b in m.stateToId){if(!m.stateToId.hasOwnProperty(b))continue;a.stateToId[b]=m.stateToId[b]}m.store=a,m.normalizeStore(),f.setItem("History.store",k.stringify(a))},m.intervalList.push(i(m.onUnload,m.options.storeInterval)),m.Adapter.bind(a,"beforeunload",m.onUnload),m.Adapter.bind(a,"unload",m.onUnload));if(!m.emulated.pushState){m.bugs.safariPoll&&m.intervalList.push(i(m.safariStatePoll,m.options.safariPollInterval));if(e.vendor==="Apple Computer, Inc."||(e.appCodeName||"")==="Mozilla")m.Adapter.bind(a,"hashchange",function(){m.Adapter.trigger(a,"popstate")}),m.getHash()&&m.Adapter.onDomLoad(function(){m.Adapter.trigger(a,"hashchange")})}},m.init()}(window) \ No newline at end of file +window.JSON || (window.JSON = {}), (function () { + function f(a) { + return a < 10 ? '0' + a : a; + } + + function quote(a) { + return escapable.lastIndex = 0, escapable.test(a) ? '"' + a.replace(escapable, a => { + const b = meta[a]; return typeof b === 'string' ? b : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + a + '"'; + } + + function str(a, b) { + let c; let d; let e; let f; const g = gap; let h; let i = b[a]; i && typeof i === 'object' && typeof i.toJSON === 'function' && (i = i.toJSON(a)), typeof rep === 'function' && (i = rep.call(b, a, i)); switch (typeof i) { + case 'string': return quote(i); case 'number': return isFinite(i) ? String(i) : 'null'; case 'boolean': case 'null': return String(i); case 'object': if (!i) { + return 'null'; + } + + gap += indent, h = []; if (Object.prototype.toString.apply(i) === '[object Array]') { + f = i.length; for (c = 0; c < f; c += 1) { + h[c] = str(c, i) || 'null'; + } + + return e = h.length === 0 ? '[]' : (gap ? '[\n' + gap + h.join(',\n' + gap) + '\n' + g + ']' : '[' + h.join(',') + ']'), gap = g, e; + } + + if (rep && typeof rep === 'object') { + f = rep.length; for (c = 0; c < f; c += 1) { + d = rep[c], typeof d === 'string' && (e = str(d, i), e && h.push(quote(d) + (gap ? ': ' : ':') + e)); + } + } else { + for (d in i) { + Object.hasOwnProperty.call(i, d) && (e = str(d, i), e && h.push(quote(d) + (gap ? ': ' : ':') + e)); + } + } + + return e = h.length === 0 ? '{}' : (gap ? '{\n' + gap + h.join(',\n' + gap) + '\n' + g + '}' : '{' + h.join(',') + '}'), gap = g, e; + } + } + + 'use strict', typeof Date.prototype.toJSON !== 'function' && (Date.prototype.toJSON = function (a) { + return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; + }, String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (a) { + return this.valueOf(); + }); const {JSON} = window; const cx = /[\u0000\u00AD\u0600-\u0604\u070F\u17B4\u17B5\u200C-\u200F\u2028-\u202F\u2060-\u206f\ufeff\ufff0-\uffff]/g; var escapable = /[\\\"\u0000-\u001F\u007F-\u009F\u00AD\u0600-\u0604\u070F\u17B4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; let gap; let indent; var meta = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"': '\\"', '\\': '\\\\'}; let + rep; typeof JSON.stringify !== 'function' && (JSON.stringify = function (a, b, c) { + let d; gap = '', indent = ''; if (typeof c === 'number') { + for (d = 0; d < c; d += 1) { + indent += ' '; + } + } else { + typeof c === 'string' && (indent = c); + } + + rep = b; if (!b || typeof b === 'function' || typeof b === 'object' && typeof b.length === 'number') { + return str('', {'': a}); + } + + throw new Error('JSON.stringify'); + }), typeof JSON.parse !== 'function' && (JSON.parse = function (text, reviver) { + function walk(a, b) { + let c; let d; const e = a[b]; if (e && typeof e === 'object') { + for (c in e) { + Object.hasOwnProperty.call(e, c) && (d = walk(e, c), d !== undefined ? e[c] = d : delete e[c]); + } + } + + return reviver.call(a, b, e); + } + + let j; text = String(text), cx.lastIndex = 0, cx.test(text) && (text = text.replace(cx, a => { + return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + })); if (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + return j = eval('(' + text + ')'), typeof reviver === 'function' ? walk({'': j}, '') : j; + } + + throw new SyntaxError('JSON.parse'); + }); +})(), (function (a, b) { + 'use strict'; const c = a.History = a.History || {}; const d = a.Zepto; if (typeof c.Adapter !== 'undefined') { + throw new TypeError('History.js Adapter has already been loaded...'); + } + + c.Adapter = {bind(a, b, c) { + (new d(a)).bind(b, c); + }, trigger(a, b) { + (new d(a)).trigger(b); + }, extractEventData(a, c) { + const d = c && c[a] || b; return d; + }, onDomLoad(a) { + new d(a); + }}, typeof c.init !== 'undefined' && c.init(); +})(window), (function (a, b) { + 'use strict'; const c = a.document; var d = a.setTimeout || d; var e = a.clearTimeout || e; var f = a.setInterval || f; const g = a.History = a.History || {}; if (typeof g.initHtml4 !== 'undefined') { + throw new TypeError('History.js HTML4 Support has already been loaded...'); + } + + g.initHtml4 = function () { + if (typeof g.initHtml4.initialized !== 'undefined') { + return !1; + } + + g.initHtml4.initialized = !0, g.enabled = !0, g.savedHashes = [], g.isLastHash = function (a) { + const b = g.getHashByIndex(); let c; return c = a === b, c; + }, g.saveHash = function (a) { + return g.isLastHash(a) ? !1 : (g.savedHashes.push(a), !0); + }, g.getHashByIndex = function (a) { + let b = null; return typeof a === 'undefined' ? b = g.savedHashes[g.savedHashes.length - 1] : (a < 0 ? b = g.savedHashes[g.savedHashes.length + a] : b = g.savedHashes[a]), b; + }, g.discardedHashes = {}, g.discardedStates = {}, g.discardState = function (a, b, c) { + const d = g.getHashByState(a); let e; return e = {discardedState: a, backState: c, forwardState: b}, g.discardedStates[d] = e, !0; + }, g.discardHash = function (a, b, c) { + const d = {discardedHash: a, backState: c, forwardState: b}; return g.discardedHashes[a] = d, !0; + }, g.discardedState = function (a) { + const b = g.getHashByState(a); let c; return c = g.discardedStates[b] || !1, c; + }, g.discardedHash = function (a) { + const b = g.discardedHashes[a] || !1; return b; + }, g.recycleState = function (a) { + const b = g.getHashByState(a); return g.discardedState(a) && delete g.discardedStates[b], !0; + }, g.emulated.hashChange && (g.hashChangeInit = function () { + g.checkerFunction = null; let b = ''; let d; let e; let h; let i; return g.isInternetExplorer() ? (d = 'historyjs-iframe', e = c.createElement('iframe'), e.setAttribute('id', d), e.style.display = 'none', c.body.appendChild(e), e.contentWindow.document.open(), e.contentWindow.document.close(), h = '', i = !1, g.checkerFunction = function () { + if (i) { + return !1; + } + + i = !0; const c = g.getHash() || ''; let d = g.unescapeHash(e.contentWindow.document.location.hash) || ''; return c !== b ? (b = c, d !== c && (h = d = c, e.contentWindow.document.open(), e.contentWindow.document.close(), e.contentWindow.document.location.hash = g.escapeHash(c)), g.Adapter.trigger(a, 'hashchange')) : d !== h && (h = d, g.setHash(d, !1)), i = !1, !0; + }) : g.checkerFunction = function () { + const c = g.getHash(); return c !== b && (b = c, g.Adapter.trigger(a, 'hashchange')), !0; + }, g.intervalList.push(f(g.checkerFunction, g.options.hashChangeInterval)), !0; + }, g.Adapter.onDomLoad(g.hashChangeInit)), g.emulated.pushState && (g.onHashChange = function (b) { + const d = b && b.newURL || c.location.href; const e = g.getHashByUrl(d); let f = null; let h = null; const i = null; let j; return g.isLastHash(e) ? (g.busy(!1), !1) : (g.doubleCheckComplete(), g.saveHash(e), e && g.isTraditionalAnchor(e) ? (g.Adapter.trigger(a, 'anchorchange'), g.busy(!1), !1) : (f = g.extractState(g.getFullUrl(e || c.location.href, !1), !0), g.isLastSavedState(f) ? (g.busy(!1), !1) : (h = g.getHashByState(f), j = g.discardedState(f), j ? (g.getHashByIndex(-2) === g.getHashByState(j.forwardState) ? g.back(!1) : g.forward(!1), !1) : (g.pushState(f.data, f.title, f.url, !1), !0)))); + }, g.Adapter.bind(a, 'hashchange', g.onHashChange), g.pushState = function (b, d, e, f) { + if (g.getHashByUrl(e)) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } + + if (f !== !1 && g.busy()) { + return g.pushQueue({scope: g, callback: g.pushState, args: arguments, queue: f}), !1; + } + + g.busy(!0); const h = g.createStateObject(b, d, e); const i = g.getHashByState(h); const j = g.getState(!1); const k = g.getHashByState(j); const l = g.getHash(); return g.storeState(h), g.expectedStateId = h.id, g.recycleState(h), g.setTitle(h), i === k ? (g.busy(!1), !1) : (i !== l && i !== g.getShortUrl(c.location.href) ? (g.setHash(i, !1), !1) : (g.saveState(h), g.Adapter.trigger(a, 'statechange'), g.busy(!1), !0)); + }, g.replaceState = function (a, b, c, d) { + if (g.getHashByUrl(c)) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } + + if (d !== !1 && g.busy()) { + return g.pushQueue({scope: g, callback: g.replaceState, args: arguments, queue: d}), !1; + } + + g.busy(!0); const e = g.createStateObject(a, b, c); const f = g.getState(!1); const h = g.getStateByIndex(-2); return g.discardState(f, e, h), g.pushState(e.data, e.title, e.url, !1), !0; + }), g.emulated.pushState && g.getHash() && !g.emulated.hashChange && g.Adapter.onDomLoad(() => { + g.Adapter.trigger(a, 'hashchange'); + }); + }, typeof g.init !== 'undefined' && g.init(); +})(window), (function (a, b) { + 'use strict'; const c = a.console || b; const d = a.document; const e = a.navigator; const f = a.sessionStorage || !1; const g = a.setTimeout; const h = a.clearTimeout; const i = a.setInterval; const j = a.clearInterval; const k = a.JSON; const l = a.alert; const m = a.History = a.History || {}; const n = a.history; k.stringify = k.stringify || k.encode, k.parse = k.parse || k.decode; if (typeof m.init !== 'undefined') { + throw new TypeError('History.js Core has already been loaded...'); + } + + m.init = function () { + return typeof m.Adapter === 'undefined' ? !1 : (typeof m.initCore !== 'undefined' && m.initCore(), typeof m.initHtml4 !== 'undefined' && m.initHtml4(), !0); + }, m.initCore = function () { + if (typeof m.initCore.initialized !== 'undefined') { + return !1; + } + + m.initCore.initialized = !0, m.options = m.options || {}, m.options.hashChangeInterval = m.options.hashChangeInterval || 100, m.options.safariPollInterval = m.options.safariPollInterval || 500, m.options.doubleCheckInterval = m.options.doubleCheckInterval || 500, m.options.storeInterval = m.options.storeInterval || 1e3, m.options.busyDelay = m.options.busyDelay || 250, m.options.debug = m.options.debug || !1, m.options.initialTitle = m.options.initialTitle || d.title, m.intervalList = [], m.clearAllIntervals = function () { + let a; const b = m.intervalList; if (typeof b !== 'undefined' && b !== null) { + for (a = 0; a < b.length; a++) { + j(b[a]); + } + + m.intervalList = null; + } + }, m.debug = function () { + (m.options.debug || !1) && m.log.apply(m, arguments); + }, m.log = function () { + const a = typeof c !== 'undefined' && typeof c.log !== 'undefined' && typeof c.log.apply !== 'undefined'; const b = d.querySelector('#log'); let e; let f; let g; let h; let i; a ? (h = Array.prototype.slice.call(arguments), e = h.shift(), typeof c.debug !== 'undefined' ? c.debug.apply(c, [e, h]) : c.log.apply(c, [e, h])) : e = '\n' + arguments[0] + '\n'; for (f = 1, g = arguments.length; f < g; ++f) { + i = arguments[f]; if (typeof i === 'object' && typeof k !== 'undefined') { + try { + i = k.stringify(i); + } catch (error) {} + } + + e += '\n' + i + '\n'; + } + + return b ? (b.value += e + '\n-----\n', b.scrollTop = b.scrollHeight - b.clientHeight) : a || l(e), !0; + }, m.getInternetExplorerMajorVersion = function () { + const a = m.getInternetExplorerMajorVersion.cached = typeof m.getInternetExplorerMajorVersion.cached !== 'undefined' ? m.getInternetExplorerMajorVersion.cached : (function () { + let a = 3; const b = d.createElement('div'); const c = b.querySelectorAll('i'); while ((b.innerHTML = '') && c[0]) { + + } + + return a > 4 ? a : !1; + })(); return a; + }, m.isInternetExplorer = function () { + const a = m.isInternetExplorer.cached = typeof m.isInternetExplorer.cached !== 'undefined' ? m.isInternetExplorer.cached : Boolean(m.getInternetExplorerMajorVersion()); return a; + }, m.emulated = {pushState: !(a.history && a.history.pushState && a.history.replaceState && !/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent) && !/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)), hashChange: Boolean(!('onhashchange' in a || 'onhashchange' in d) || m.isInternetExplorer() && m.getInternetExplorerMajorVersion() < 8)}, m.enabled = !m.emulated.pushState, m.bugs = {setHash: Boolean(!m.emulated.pushState && e.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)), safariPoll: Boolean(!m.emulated.pushState && e.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)), ieDoubleCheck: Boolean(m.isInternetExplorer() && m.getInternetExplorerMajorVersion() < 8), hashEscape: Boolean(m.isInternetExplorer() && m.getInternetExplorerMajorVersion() < 7)}, m.isEmptyObject = function (a) { + for (const b in a) { + return !1; + } + + return !0; + }, m.cloneObject = function (a) { + let b; let c; return a ? (b = k.stringify(a), c = k.parse(b)) : c = {}, c; + }, m.getRootUrl = function () { + let a = d.location.protocol + '//' + (d.location.hostname || d.location.host); if (d.location.port || !1) { + a += ':' + d.location.port; + } + + return a += '/', a; + }, m.getBaseHref = function () { + const a = d.querySelectorAll('base'); let b = null; let c = ''; return a.length === 1 && (b = a[0], c = b.href.replace(/[^\/]+$/, '')), c = c.replace(/\/+$/, ''), c && (c += '/'), c; + }, m.getBaseUrl = function () { + const a = m.getBaseHref() || m.getBasePageUrl() || m.getRootUrl(); return a; + }, m.getPageUrl = function () { + const a = m.getState(!1, !1); const b = (a || {}).url || d.location.href; let c; return c = b.replace(/\/+$/, '').replace(/[^\/]+$/, (a, b, c) => { + return /\./.test(a) ? a : a + '/'; + }), c; + }, m.getBasePageUrl = function () { + const a = d.location.href.replace(/[#\?].*/, '').replace(/[^\/]+$/, (a, b, c) => { + return /[^\/]$/.test(a) ? '' : a; + }).replace(/\/+$/, '') + '/'; return a; + }, m.getFullUrl = function (a, b) { + let c = a; const d = a.slice(0, 1); return b = typeof b === 'undefined' ? !0 : b, /[a-z]+\:\/\//.test(a) || (d === '/' ? c = m.getRootUrl() + a.replace(/^\/+/, '') : d === '#' ? c = m.getPageUrl().replace(/#.*/, '') + a : d === '?' ? c = m.getPageUrl().replace(/[\?#].*/, '') + a : (b ? c = m.getBaseUrl() + a.replace(/^(\.\/)+/, '') : c = m.getBasePageUrl() + a.replace(/^(\.\/)+/, ''))), c.replace(/\#$/, ''); + }, m.getShortUrl = function (a) { + let b = a; const c = m.getBaseUrl(); const d = m.getRootUrl(); return m.emulated.pushState && (b = b.replace(c, '')), b = b.replace(d, '/'), m.isTraditionalAnchor(b) && (b = './' + b), b = b.replace(/^(\.\/)+/g, './').replace(/\#$/, ''), b; + }, m.store = {}, m.idToState = m.idToState || {}, m.stateToId = m.stateToId || {}, m.urlToId = m.urlToId || {}, m.storedStates = m.storedStates || [], m.savedStates = m.savedStates || [], m.normalizeStore = function () { + m.store.idToState = m.store.idToState || {}, m.store.urlToId = m.store.urlToId || {}, m.store.stateToId = m.store.stateToId || {}; + }, m.getState = function (a, b) { + typeof a === 'undefined' && (a = !0), typeof b === 'undefined' && (b = !0); let c = m.getLastSavedState(); return !c && b && (c = m.createStateObject()), a && (c = m.cloneObject(c), c.url = c.cleanUrl || c.url), c; + }, m.getIdByState = function (a) { + let b = m.extractId(a.url); let c; if (!b) { + c = m.getStateString(a); if (typeof m.stateToId[c] !== 'undefined') { + b = m.stateToId[c]; + } else if (typeof m.store.stateToId[c] !== 'undefined') { + b = m.store.stateToId[c]; + } else { + for (;;) { + b = (new Date()).getTime() + String(Math.random()).replace(/\D/g, ''); if (typeof m.idToState[b] === 'undefined' && typeof m.store.idToState[b] === 'undefined') { + break; + } + } + + m.stateToId[c] = b, m.idToState[b] = a; + } + } + + return b; + }, m.normalizeState = function (a) { + let b; let c; if (!a || typeof a !== 'object') { + a = {}; + } + + if (typeof a.normalized !== 'undefined') { + return a; + } + + if (!a.data || typeof a.data !== 'object') { + a.data = {}; + } + + b = {}, b.normalized = !0, b.title = a.title || '', b.url = m.getFullUrl(m.unescapeString(a.url || d.location.href)), b.hash = m.getShortUrl(b.url), b.data = m.cloneObject(a.data), b.id = m.getIdByState(b), b.cleanUrl = b.url.replace(/\??\&_suid.*/, ''), b.url = b.cleanUrl, c = !m.isEmptyObject(b.data); if (b.title || c) { + b.hash = m.getShortUrl(b.url).replace(/\??\&_suid.*/, ''), /\?/.test(b.hash) || (b.hash += '?'), b.hash += '&_suid=' + b.id; + } + + return b.hashedUrl = m.getFullUrl(b.hash), (m.emulated.pushState || m.bugs.safariPoll) && m.hasUrlDuplicate(b) && (b.url = b.hashedUrl), b; + }, m.createStateObject = function (a, b, c) { + let d = {data: a, title: b, url: c}; return d = m.normalizeState(d), d; + }, m.getStateById = function (a) { + a = String(a); const c = m.idToState[a] || m.store.idToState[a] || b; return c; + }, m.getStateString = function (a) { + let b; let c; let d; return b = m.normalizeState(a), c = {data: b.data, title: a.title, url: a.url}, d = k.stringify(c), d; + }, m.getStateId = function (a) { + let b; let c; return b = m.normalizeState(a), c = b.id, c; + }, m.getHashByState = function (a) { + let b; let c; return b = m.normalizeState(a), c = b.hash, c; + }, m.extractId = function (a) { + let b; let c; let d; return c = /(.*)\&_suid=([0-9]+)$/.exec(a), d = c ? c[1] || a : a, b = c ? String(c[2] || '') : '', b || !1; + }, m.isTraditionalAnchor = function (a) { + const b = !/[\/\?\.]/.test(a); return b; + }, m.extractState = function (a, b) { + let c = null; let d; let e; return b = b || !1, d = m.extractId(a), d && (c = m.getStateById(d)), c || (e = m.getFullUrl(a), d = m.getIdByUrl(e) || !1, d && (c = m.getStateById(d)), !c && b && !m.isTraditionalAnchor(a) && (c = m.createStateObject(null, null, e))), c; + }, m.getIdByUrl = function (a) { + const c = m.urlToId[a] || m.store.urlToId[a] || b; return c; + }, m.getLastSavedState = function () { + return m.savedStates[m.savedStates.length - 1] || b; + }, m.getLastStoredState = function () { + return m.storedStates[m.storedStates.length - 1] || b; + }, m.hasUrlDuplicate = function (a) { + let b = !1; let c; return c = m.extractState(a.url), b = c && c.id !== a.id, b; + }, m.storeState = function (a) { + return m.urlToId[a.url] = a.id, m.storedStates.push(m.cloneObject(a)), a; + }, m.isLastSavedState = function (a) { + let b = !1; let c; let d; let e; return m.savedStates.length && (c = a.id, d = m.getLastSavedState(), e = d.id, b = c === e), b; + }, m.saveState = function (a) { + return m.isLastSavedState(a) ? !1 : (m.savedStates.push(m.cloneObject(a)), !0); + }, m.getStateByIndex = function (a) { + let b = null; return typeof a === 'undefined' ? b = m.savedStates[m.savedStates.length - 1] : (a < 0 ? b = m.savedStates[m.savedStates.length + a] : b = m.savedStates[a]), b; + }, m.getHash = function () { + const a = m.unescapeHash(d.location.hash); return a; + }, m.unescapeString = function (b) { + let c = b; let d; for (;;) { + d = a.unescape(c); if (d === c) { + break; + } + + c = d; + } + + return c; + }, m.unescapeHash = function (a) { + let b = m.normalizeHash(a); return b = m.unescapeString(b), b; + }, m.normalizeHash = function (a) { + const b = a.replace(/[^#]*#/, '').replace(/#.*/, ''); return b; + }, m.setHash = function (a, b) { + let c; let e; let f; return b !== !1 && m.busy() ? (m.pushQueue({scope: m, callback: m.setHash, args: arguments, queue: b}), !1) : (c = m.escapeHash(a), m.busy(!0), e = m.extractState(a, !0), e && !m.emulated.pushState ? m.pushState(e.data, e.title, e.url, !1) : d.location.hash !== c && (m.bugs.setHash ? (f = m.getPageUrl(), m.pushState(null, null, f + '#' + c, !1)) : d.location.hash = c), m); + }, m.escapeHash = function (b) { + let c = m.normalizeHash(b); return c = a.escape(c), m.bugs.hashEscape || (c = c.replace(/\%21/g, '!').replace(/\%26/g, '&').replace(/\%3D/g, '=').replace(/\%3F/g, '?')), c; + }, m.getHashByUrl = function (a) { + let b = String(a).replace(/([^#]*)#?([^#]*)#?(.*)/, '$2'); return b = m.unescapeHash(b), b; + }, m.setTitle = function (a) { + let b = a.title; let c; b || (c = m.getStateByIndex(0), c && c.url === a.url && (b = c.title || m.options.initialTitle)); try { + d.querySelectorAll('title')[0].innerHTML = b.replace('<', '<').replace('>', '>').replace(' & ', ' & '); + } catch (error) {} + + return d.title = b, m; + }, m.queues = [], m.busy = function (a) { + typeof a !== 'undefined' ? m.busy.flag = a : typeof m.busy.flag === 'undefined' && (m.busy.flag = !1); if (!m.busy.flag) { + h(m.busy.timeout); var b = function () { + let a; let c; let d; if (m.busy.flag) { + return; + } + + for (a = m.queues.length - 1; a >= 0; --a) { + c = m.queues[a]; if (c.length === 0) { + continue; + } + + d = c.shift(), m.fireQueueItem(d), m.busy.timeout = g(b, m.options.busyDelay); + } + }; + + m.busy.timeout = g(b, m.options.busyDelay); + } + + return m.busy.flag; + }, m.busy.flag = !1, m.fireQueueItem = function (a) { + return a.callback.apply(a.scope || m, a.args || []); + }, m.pushQueue = function (a) { + return m.queues[a.queue || 0] = m.queues[a.queue || 0] || [], m.queues[a.queue || 0].push(a), m; + }, m.queue = function (a, b) { + return typeof a === 'function' && (a = {callback: a}), typeof b !== 'undefined' && (a.queue = b), m.busy() ? m.pushQueue(a) : m.fireQueueItem(a), m; + }, m.clearQueue = function () { + return m.busy.flag = !1, m.queues = [], m; + }, m.stateChanged = !1, m.doubleChecker = !1, m.doubleCheckComplete = function () { + return m.stateChanged = !0, m.doubleCheckClear(), m; + }, m.doubleCheckClear = function () { + return m.doubleChecker && (h(m.doubleChecker), m.doubleChecker = !1), m; + }, m.doubleCheck = function (a) { + return m.stateChanged = !1, m.doubleCheckClear(), m.bugs.ieDoubleCheck && (m.doubleChecker = g(() => { + return m.doubleCheckClear(), m.stateChanged || a(), !0; + }, m.options.doubleCheckInterval)), m; + }, m.safariStatePoll = function () { + const b = m.extractState(d.location.href); let c; if (!m.isLastSavedState(b)) { + c = b; + } else { + return; + } + + return c || (c = m.createStateObject()), m.Adapter.trigger(a, 'popstate'), m; + }, m.back = function (a) { + return a !== !1 && m.busy() ? (m.pushQueue({scope: m, callback: m.back, args: arguments, queue: a}), !1) : (m.busy(!0), m.doubleCheck(() => { + m.back(!1); + }), n.go(-1), !0); + }, m.forward = function (a) { + return a !== !1 && m.busy() ? (m.pushQueue({scope: m, callback: m.forward, args: arguments, queue: a}), !1) : (m.busy(!0), m.doubleCheck(() => { + m.forward(!1); + }), n.go(1), !0); + }, m.go = function (a, b) { + let c; if (a > 0) { + for (c = 1; c <= a; ++c) { + m.forward(b); + } + } else { + if (!(a < 0)) { + throw new Error('History.go: History.go requires a positive or negative integer passed.'); + } + + for (c = -1; c >= a; --c) { + m.back(b); + } + } + + return m; + }; + + if (m.emulated.pushState) { + const o = function () {}; m.pushState = m.pushState || o, m.replaceState = m.replaceState || o; + } else { + m.onPopState = function (b, c) { + let e = !1; let f = !1; let g; let h; return m.doubleCheckComplete(), g = m.getHash(), g ? (h = m.extractState(g || d.location.href, !0), h ? m.replaceState(h.data, h.title, h.url, !1) : (m.Adapter.trigger(a, 'anchorchange'), m.busy(!1)), m.expectedStateId = !1, !1) : (e = m.Adapter.extractEventData('state', b, c) || !1, e ? f = m.getStateById(e) : (m.expectedStateId ? f = m.getStateById(m.expectedStateId) : f = m.extractState(d.location.href)), f || (f = m.createStateObject(null, null, d.location.href)), m.expectedStateId = !1, m.isLastSavedState(f) ? (m.busy(!1), !1) : (m.storeState(f), m.saveState(f), m.setTitle(f), m.Adapter.trigger(a, 'statechange'), m.busy(!1), !0)); + }, m.Adapter.bind(a, 'popstate', m.onPopState), m.pushState = function (b, c, d, e) { + if (m.getHashByUrl(d) && m.emulated.pushState) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } + + if (e !== !1 && m.busy()) { + return m.pushQueue({scope: m, callback: m.pushState, args: arguments, queue: e}), !1; + } + + m.busy(!0); const f = m.createStateObject(b, c, d); return m.isLastSavedState(f) ? m.busy(!1) : (m.storeState(f), m.expectedStateId = f.id, n.pushState(f.id, f.title, f.url), m.Adapter.trigger(a, 'popstate')), !0; + }, m.replaceState = function (b, c, d, e) { + if (m.getHashByUrl(d) && m.emulated.pushState) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } + + if (e !== !1 && m.busy()) { + return m.pushQueue({scope: m, callback: m.replaceState, args: arguments, queue: e}), !1; + } + + m.busy(!0); const f = m.createStateObject(b, c, d); return m.isLastSavedState(f) ? m.busy(!1) : (m.storeState(f), m.expectedStateId = f.id, n.replaceState(f.id, f.title, f.url), m.Adapter.trigger(a, 'popstate')), !0; + }; + } + + if (f) { + try { + m.store = k.parse(f.getItem('History.store')) || {}; + } catch (error) { + m.store = {}; + } + + m.normalizeStore(); + } else { + m.store = {}, m.normalizeStore(); + } + + m.Adapter.bind(a, 'beforeunload', m.clearAllIntervals), m.Adapter.bind(a, 'unload', m.clearAllIntervals), m.saveState(m.storeState(m.extractState(d.location.href, !0))), f && (m.onUnload = function () { + let a; let b; try { + a = k.parse(f.getItem('History.store')) || {}; + } catch (error) { + a = {}; + } + + a.idToState = a.idToState || {}, a.urlToId = a.urlToId || {}, a.stateToId = a.stateToId || {}; for (b in m.idToState) { + if (!m.idToState.hasOwnProperty(b)) { + continue; + } + + a.idToState[b] = m.idToState[b]; + } + + for (b in m.urlToId) { + if (!m.urlToId.hasOwnProperty(b)) { + continue; + } + + a.urlToId[b] = m.urlToId[b]; + } + + for (b in m.stateToId) { + if (!m.stateToId.hasOwnProperty(b)) { + continue; + } + + a.stateToId[b] = m.stateToId[b]; + } + + m.store = a, m.normalizeStore(), f.setItem('History.store', k.stringify(a)); + }, m.intervalList.push(i(m.onUnload, m.options.storeInterval)), m.Adapter.bind(a, 'beforeunload', m.onUnload), m.Adapter.bind(a, 'unload', m.onUnload)); if (!m.emulated.pushState) { + m.bugs.safariPoll && m.intervalList.push(i(m.safariStatePoll, m.options.safariPollInterval)); if (e.vendor === 'Apple Computer, Inc.' || (e.appCodeName || '') === 'Mozilla') { + m.Adapter.bind(a, 'hashchange', () => { + m.Adapter.trigger(a, 'popstate'); + }), m.getHash() && m.Adapter.onDomLoad(() => { + m.Adapter.trigger(a, 'hashchange'); + }); + } + } + }, m.init(); +})(window); diff --git a/demos/twitter_backbone/app.js b/demos/twitter_backbone/app.js index 747af372..f96e1473 100644 --- a/demos/twitter_backbone/app.js +++ b/demos/twitter_backbone/app.js @@ -1,39 +1,40 @@ -var http = require("http"), - url = require("url"), - path = require("path"), - fs = require("fs") - port = process.argv[2] || process.env.PORT || 8888, - host = process.argv[3] || process.env.IP || "0.0.0.0"; - -http.createServer(function(request, response) { - - var uri = url.parse(request.url).pathname - , filename = path.join(__dirname, uri); - - path.exists(filename, function(exists) { - if(!exists) { - response.writeHead(404, {"Content-Type": "text/plain"}); - response.write("404 Not Found\n"); - response.end(); - return; - } - -if (fs.statSync(filename).isDirectory()) filename += '/index.html'; - - fs.readFile(filename, "binary", function(err, file) { - if(err) { - response.writeHead(500, {"Content-Type": "text/plain"}); - response.write(err + "\n"); - response.end(); - return; - } - - response.writeHead(200); - response.write(file, "binary"); - response.end(); +const http = require('http'); +const url = require('url'); +const path = require('path'); +const fs = require('fs'); +port = process.argv[2] || process.env.PORT || 8888, +host = process.argv[3] || process.env.IP || '0.0.0.0'; + +http.createServer((request, response) => { + const uri = url.parse(request.url).pathname; + let filename = path.join(__dirname, uri); + + path.exists(filename, exists => { + if (!exists) { + response.writeHead(404, {'Content-Type': 'text/plain'}); + response.write('404 Not Found\n'); + response.end(); + return; + } + + if (fs.statSync(filename).isDirectory()) { + filename += '/index.html'; + } + + fs.readFile(filename, 'binary', (err, file) => { + if (err) { + response.writeHead(500, {'Content-Type': 'text/plain'}); + response.write(err + '\n'); + response.end(); + return; + } + + response.writeHead(200); + response.write(file, 'binary'); + response.end(); + }); }); - }); }).listen(parseInt(port, 10), host); -console.log("Twig.Twitter demo running at\n => " + host + ":" + port + "/\nCTRL + C to shutdown"); +console.log('Twig.Twitter demo running at\n => ' + host + ':' + port + '/\nCTRL + C to shutdown'); diff --git a/demos/twitter_backbone/js/app.js b/demos/twitter_backbone/js/app.js index c7429095..96eea7e6 100644 --- a/demos/twitter_backbone/js/app.js +++ b/demos/twitter_backbone/js/app.js @@ -5,39 +5,39 @@ module.declare( [ - { settings: "js/model/settings" } - , { appView: "js/view/appView" } + {settings: 'js/model/settings'}, + {appView: 'js/view/appView'} ] - , function (require, exports, module) { - var Settings = require("settings").Settings - , AppView = require("appView").AppView + , (require, exports, module) => { + const {Settings} = require('settings'); + const {AppView} = require('appView') // Models - , settingId = 0 - , settings = new Settings; + ; const settingId = 0; + const settings = new Settings(); // Load from local storage settings.fetch(); - - var setting = settings.get(settingId), - username = null; + + let setting = settings.get(settingId); + let username = null; // Initialize the settings model with a username - if (!setting || !setting.get("username")) { - username = prompt("Please enter a twitter username:"); + if (!setting || !setting.get('username')) { + username = prompt('Please enter a twitter username:'); setting = settings.create({ - "id": settingId - , "username": username + id: settingId, + username }); setting.save(); } // Create the view and kick off the application - var appView = new AppView({ + const appView = new AppView({ model: setting }); - $("body").append(appView.el); + $('body').append(appView.el); } ); diff --git a/demos/twitter_backbone/js/model/account.js b/demos/twitter_backbone/js/model/account.js index 34d6ac53..b56c19e2 100644 --- a/demos/twitter_backbone/js/model/account.js +++ b/demos/twitter_backbone/js/model/account.js @@ -1,10 +1,10 @@ module.declare( [ - { backbone: 'vendor/backbone' } + {backbone: 'vendor/backbone'} ] - , function(require, exports, module) { - var Backbone = require('backbone') - , Account = Backbone.Model.extend({ }); + , (require, exports, module) => { + const Backbone = require('backbone'); + const Account = Backbone.Model.extend({ }); return Account; } diff --git a/demos/twitter_backbone/js/model/feed.js b/demos/twitter_backbone/js/model/feed.js index 3ce67bb1..b851e557 100644 --- a/demos/twitter_backbone/js/model/feed.js +++ b/demos/twitter_backbone/js/model/feed.js @@ -1,46 +1,47 @@ module.declare( [ - { backbone: 'vendor/backbone' } - , { underscore: 'vendor/underscore' } - , { tweet: "js/model/tweet" } + {backbone: 'vendor/backbone'}, + {underscore: 'vendor/underscore'}, + {tweet: 'js/model/tweet'} ], - function(require, exports, module) { - var Backbone = require("backbone") - , _ = require("underscore")._ - , Tweet = require("tweet").Tweet - , Feed = Backbone.Collection.extend({ - localStorage: new Backbone.Store("tweets") - , model: Tweet + (require, exports, module) => { + const Backbone = require('backbone'); + const {_} = require('underscore'); + const {Tweet} = require('tweet'); + const Feed = Backbone.Collection.extend({ + localStorage: new Backbone.Store('tweets'), + model: Tweet, - , loadUser: function(username) { - var that = this - , request; - while(this.length > 0) { - this.each(function(tweet){ - tweet.destroy(); - }); - } - request = $.ajax({ - url: 'https://api.twitter.com/1/statuses/user_timeline.json?callback=?' - , dataType: 'json' - , data: { - include_entities: "true" - , include_rts: "true" - , screen_name: username - } + loadUser(username) { + const that = this; + let request; + while (this.length > 0) { + this.each(tweet => { + tweet.destroy(); }); + } - request.done(function(data) { - _.each(data, function(tweet) { - var newTweet = that.create(tweet); - }); - }); + request = $.ajax({ + url: 'https://api.twitter.com/1/statuses/user_timeline.json?callback=?', + dataType: 'json', + data: { + include_entities: 'true', + include_rts: 'true', + screen_name: username + } + }); - request.error(function(jqXHR, status) { - alert("Unable to load tweets, error:\n" + status); + request.done(data => { + _.each(data, tweet => { + const newTweet = that.create(tweet); }); - } - }); - exports.feed = new Feed; + }); + + request.error((jqXHR, status) => { + alert('Unable to load tweets, error:\n' + status); + }); + } + }); + exports.feed = new Feed(); } ); diff --git a/demos/twitter_backbone/js/model/settings.js b/demos/twitter_backbone/js/model/settings.js index 3f8803d3..eabf4b91 100644 --- a/demos/twitter_backbone/js/model/settings.js +++ b/demos/twitter_backbone/js/model/settings.js @@ -1,15 +1,15 @@ // Settings model and collection module.declare( [ - { backbone: 'vendor/backbone' } + {backbone: 'vendor/backbone'} ] - , function(require, exports, module) { - var Backbone = require("backbone") - , Setting = Backbone.Model.extend({ }) - , Settings = Backbone.Collection.extend({ - model: Setting - , localStorage: new Backbone.Store("settings") - }); + , (require, exports, module) => { + const Backbone = require('backbone'); + const Setting = Backbone.Model.extend({ }); + const Settings = Backbone.Collection.extend({ + model: Setting, + localStorage: new Backbone.Store('settings') + }); exports.Setting = Setting; exports.Settings = Settings; diff --git a/demos/twitter_backbone/js/model/tweet.js b/demos/twitter_backbone/js/model/tweet.js index b4f7d159..6bcb6c20 100644 --- a/demos/twitter_backbone/js/model/tweet.js +++ b/demos/twitter_backbone/js/model/tweet.js @@ -1,11 +1,11 @@ // Tweet Model module.declare( [ - { backbone: 'vendor/backbone' } + {backbone: 'vendor/backbone'} ] - , function(require, exports, module) { - var Backbone = require("backbone") - , Tweet = Backbone.Model.extend({ }); + , (require, exports, module) => { + const Backbone = require('backbone'); + const Tweet = Backbone.Model.extend({ }); exports.Tweet = Tweet; } diff --git a/demos/twitter_backbone/js/view/appView.js b/demos/twitter_backbone/js/view/appView.js index a1ff85dc..197333d5 100644 --- a/demos/twitter_backbone/js/view/appView.js +++ b/demos/twitter_backbone/js/view/appView.js @@ -8,86 +8,85 @@ module.declare( [ - { backbone: 'vendor/backbone' } - , { twig: "vendor/twig" } - , { feed: "js/model/feed" } - , { feedView: "js/view/feedView" } + {backbone: 'vendor/backbone'}, + {twig: 'vendor/twig'}, + {feed: 'js/model/feed'}, + {feedView: 'js/view/feedView'} ] - , function (require, exports, module) { - var twig = require("twig").twig - , Backbone = require("backbone") - , feed = require("feed").feed + , (require, exports, module) => { + const {twig} = require('twig'); + const Backbone = require('backbone'); + const {feed} = require('feed') // The application template - , template = twig({ - href: 'templates/app.twig' - , async: false - }) - - , FeedView = require("feedView").FeedView - , feedView = new FeedView + ; const template = twig({ + href: 'templates/app.twig', + async: false + }); + const {FeedView} = require('feedView'); + const feedView = new FeedView(); + const AppView = Backbone.View.extend({ + tagName: 'div', + className: 'app', - , AppView = Backbone.View.extend({ - tagName: "div" - , className: "app" + // Bind to the buttons in the template + events: { + 'click .reloadTweets': 'reload', + 'click .changeUser': 'changeUser', + 'click .twitter_user': 'twitterLink' + }, - // Bind to the buttons in the template - , events: { - "click .reloadTweets": "reload" - , "click .changeUser": "changeUser" - , "click .twitter_user": "twitterLink" - } + // Initialize the Application + initialize() { + this.model.bind('change', this.changeSettings, this); + this.feedView = feedView; + this.changeSettings(); + }, - // Initialize the Application - , initialize: function() { - this.model.bind("change", this.changeSettings, this); - this.feedView = feedView; - this.changeSettings(); - } + // Render the template with the contents of the Setting model + render() { + $(this.el).html(template.render(this.model.toJSON())); - // Render the template with the contents of the Setting model - , render: function() { - $(this.el).html(template.render(this.model.toJSON())); + this.$('.feedContainer').html(this.feedView.el); + }, - this.$(".feedContainer").html(this.feedView.el); - } + // Trigger the feed Collection to refresh the twitter feed + reload() { + const username = this.model.get('username'); + feed.loadUser(username); + }, - // Trigger the feed Collection to refresh the twitter feed - , reload: function() { - var username = this.model.get("username"); - feed.loadUser(username); - } + // Update the Setting model associated with this AppView + // The change event will trigger a redraw + changeUser() { + const username = prompt('Please enter a twitter username:'); + this.model.set({ + username + }); + this.model.save(); + }, - // Update the Setting model associated with this AppView - // The change event will trigger a redraw - , changeUser: function() { - var username = prompt("Please enter a twitter username:"); + twitterLink(e) { + const username = $(e.target).attr('user'); + if (username) { this.model.set({ - username: username + username }); this.model.save(); } - - , twitterLink: function(e) { - var username = $(e.target).attr("user"); - if (username) { - this.model.set({ - username: username - }); - this.model.save(); - } - e.preventDefault(); - e.stopPropagation(); - } - // Handle change events from the Setting model - // Renders the view and triggers a reload of the feed - , changeSettings: function() { - this.render(); - this.reload(); - } - }); - + e.preventDefault(); + e.stopPropagation(); + }, + + // Handle change events from the Setting model + // Renders the view and triggers a reload of the feed + changeSettings() { + this.render(); + this.reload(); + } + }); + exports.AppView = AppView; } ); diff --git a/demos/twitter_backbone/js/view/feedView.js b/demos/twitter_backbone/js/view/feedView.js index e6ce910e..42b036af 100644 --- a/demos/twitter_backbone/js/view/feedView.js +++ b/demos/twitter_backbone/js/view/feedView.js @@ -5,52 +5,54 @@ module.declare( [ - { backbone: 'vendor/backbone' } - , { feed: "js/model/feed" } - , { tweetView: "js/view/tweetView" } + {backbone: 'vendor/backbone'}, + {feed: 'js/model/feed'}, + {tweetView: 'js/view/tweetView'} ] - , function (require, exports, module) { - var feed = require("feed").feed - , Backbone = require("backbone") - , TweetView = require("tweetView").TweetView + , (require, exports, module) => { + const {feed} = require('feed'); + const Backbone = require('backbone'); + const {TweetView} = require('tweetView') // The FeedView is a simple container of TweetViews and therefore // doesn't need a template. The ul element provided by the Backbone // View is sufficient. - , FeedView = Backbone.View.extend({ - tagName: "ul" - , className: "feed" - - , initialize: function() { - // Bind to model changes - feed.bind('add', this.addTweet, this); - feed.bind('reset', this.addAll, this); - - // Load stored tweets from local storage - feed.fetch(); - } - - // Add a tweet to the view - // Creates a new TweetView for the tweet model - // and adds it to the FeedView - , addTweet: function(tweet) { - var tweetView = new TweetView({ - model: tweet - }); - var el = tweetView.render().el; - $(this.el).append(el); - - return this; - } - - // Handle resets to the model by adding all new elements to the view - // Existing tweet views will have been removed when their models are - // destroyed. - , addAll: function() { - var that = this; - feed.each(function(tweet){that.addTweet(tweet)}); - } - }); + ; const FeedView = Backbone.View.extend({ + tagName: 'ul', + className: 'feed', + + initialize() { + // Bind to model changes + feed.bind('add', this.addTweet, this); + feed.bind('reset', this.addAll, this); + + // Load stored tweets from local storage + feed.fetch(); + }, + + // Add a tweet to the view + // Creates a new TweetView for the tweet model + // and adds it to the FeedView + addTweet(tweet) { + const tweetView = new TweetView({ + model: tweet + }); + const {el} = tweetView.render(); + $(this.el).append(el); + + return this; + }, + + // Handle resets to the model by adding all new elements to the view + // Existing tweet views will have been removed when their models are + // destroyed. + addAll() { + const that = this; + feed.each(tweet => { + that.addTweet(tweet); + }); + } + }); exports.FeedView = FeedView; } diff --git a/demos/twitter_backbone/js/view/tweetView.js b/demos/twitter_backbone/js/view/tweetView.js index 2f7b4a1e..e92f8471 100644 --- a/demos/twitter_backbone/js/view/tweetView.js +++ b/demos/twitter_backbone/js/view/tweetView.js @@ -5,63 +5,63 @@ module.declare( [ - { backbone: 'vendor/backbone' } - , { twig: "vendor/twig" } - , { tweet: "js/model/tweet" } + {backbone: 'vendor/backbone'}, + {twig: 'vendor/twig'}, + {tweet: 'js/model/tweet'} ] - , function (require, exports, module) { - var Backbone = require("backbone") - , twig = require("twig").twig - - // Load the template for a "Tweet" - // This template only needs to be loaded once. It will be compiled at - // load time and can be rendered separately for each Tweet. - , template = twig({ - href: 'templates/tweet.twig' - , async: false - }) - - , TweetView = Backbone.View.extend({ - tagName: "li" - , className: "tweet" + , (require, exports, module) => { + const Backbone = require('backbone'); + const {twig} = require('twig'); - // Create the Tweet view - , initialize: function() { - // Re-render the tweet if the backing model changes - this.model.bind('change', this.render, this); + // Load the template for a "Tweet" + // This template only needs to be loaded once. It will be compiled at + // load time and can be rendered separately for each Tweet. + const template = twig({ + href: 'templates/tweet.twig', + async: false + }); - // Remove the Tweet if the backing model is removed. - this.model.bind('destroy', this.remove, this); - } + const TweetView = Backbone.View.extend({ + tagName: 'li', + className: 'tweet', - // Render the tweet Twig template with the contents of the model - , render: function() { - // Pass in an object representing the Tweet to serve as the - // render context for the template and inject it into the View. - $(this.el).html(template.render( - this.enhanceModel(this.model.toJSON()) - )); - return this; - } - - // Regex's for matching twitter usernames and web links - , userRegEx: /\@([a-zA-Z0-9_\-\.]+)/g - , hashRegex: /#([a-zA-Z0-9_\-\.]+)/g - , linkRegEx: /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/g - - // Enhance the model passed to the template with links - , enhanceModel: function(model) { - model.text = model.text.replace(this.linkRegEx, '$1'); - model.text = model.text.replace(this.hashRegex, '#$1'); - model.text = model.text.replace(this.userRegEx, ''); - return model; - } + // Create the Tweet view + initialize() { + // Re-render the tweet if the backing model changes + this.model.bind('change', this.render, this); - // Remove the tweet view from it's container (a FeedView) - , remove: function() { - $(this.el).remove(); - } - }); + // Remove the Tweet if the backing model is removed. + this.model.bind('destroy', this.remove, this); + }, + + // Render the tweet Twig template with the contents of the model + render() { + // Pass in an object representing the Tweet to serve as the + // render context for the template and inject it into the View. + $(this.el).html(template.render( + this.enhanceModel(this.model.toJSON()) + )); + return this; + }, + + // Regex's for matching twitter usernames and web links + userRegEx: /\@([a-zA-Z0-9_\-\.]+)/g, + hashRegex: /#([a-zA-Z0-9_\-\.]+)/g, + linkRegEx: /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/g, + + // Enhance the model passed to the template with links + enhanceModel(model) { + model.text = model.text.replace(this.linkRegEx, '$1'); + model.text = model.text.replace(this.hashRegex, '#$1'); + model.text = model.text.replace(this.userRegEx, ''); + return model; + }, + + // Remove the tweet view from it's container (a FeedView) + remove() { + $(this.el).remove(); + } + }); exports.TweetView = TweetView; } diff --git a/demos/twitter_backbone/vendor/backbone.js b/demos/twitter_backbone/vendor/backbone.js index c020cfdf..d620915f 100644 --- a/demos/twitter_backbone/vendor/backbone.js +++ b/demos/twitter_backbone/vendor/backbone.js @@ -1,56 +1,626 @@ module.declare( [ - { underscore: "vendor/underscore" } + {underscore: 'vendor/underscore'} ] , function (require, exports, module) { + // Backbone.js 0.5.3 + // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. + // Backbone may be freely distributed under the MIT license. + // For all details and documentation: + // http://documentcloud.github.com/backbone + (function () { + const h = this; const p = h.Backbone; let e; e = typeof exports !== 'undefined' ? exports : h.Backbone = {}; e.VERSION = '0.5.3'; let f = h._; if (!f && typeof require !== 'undefined') { + f = require('underscore')._; + } -// Backbone.js 0.5.3 -// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the MIT license. -// For all details and documentation: -// http://documentcloud.github.com/backbone -(function(){var h=this,p=h.Backbone,e;e=typeof exports!=="undefined"?exports:h.Backbone={};e.VERSION="0.5.3";var f=h._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var g=h.jQuery||h.Zepto;e.noConflict=function(){h.Backbone=p;return this};e.emulateHTTP=!1;e.emulateJSON=!1;e.Events={bind:function(a,b,c){var d=this._callbacks||(this._callbacks={});(d[a]||(d[a]=[])).push([b,c]);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d= -0,e=c.length;d/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")},has:function(a){return this.attributes[a]!=null},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return!1;if(this.idAttribute in a)this.id=a[this.idAttribute]; -var e=this._changing;this._changing=!0;for(var g in a){var h=a[g];if(!f.isEqual(c[g],h))c[g]=h,delete d[g],this._changed=!0,b.silent||this.trigger("change:"+g,this,h,b)}!e&&!b.silent&&this._changed&&this.change(b);this._changing=!1;return this},unset:function(a,b){if(!(a in this.attributes))return this;b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&!this._performValidation(c,b))return!1;delete this.attributes[a];delete this._escapedAttributes[a];a==this.idAttribute&&delete this.id;this._changed= -!0;b.silent||(this.trigger("change:"+a,this,void 0,b),this.change(b));return this},clear:function(a){a||(a={});var b,c=this.attributes,d={};for(b in c)d[b]=void 0;if(!a.silent&&this.validate&&!this._performValidation(d,a))return!1;this.attributes={};this._escapedAttributes={};this._changed=!0;if(!a.silent){for(b in c)this.trigger("change:"+b,this,void 0,a);this.change(a)}return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&& -c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return!1;var c=this,d=b.success;b.success=function(a,e,f){if(!c.set(c.parse(a,f),b))return!1;d&&d(c,a,f)};b.error=i(b.error,c,b);var f=this.isNew()?"create":"update";return(this.sync||e.sync).call(this,f,this,b)},destroy:function(a){a||(a={});if(this.isNew())return this.trigger("destroy",this,this.collection,a);var b=this,c=a.success;a.success=function(d){b.trigger("destroy", -b,b.collection,a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"delete",this,a)},url:function(){var a=k(this.collection)||this.urlRoot||l();if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return this.id==null},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=!1},hasChanged:function(a){if(a)return this._previousAttributes[a]!= -this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=!1,d;for(d in a)f.isEqual(b[d],a[d])||(c=c||{},c[d]=a[d]);return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c)return b.error?b.error(this,c,b):this.trigger("error",this,c,b),!1;return!0}}); -e.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;f.bindAll(this,"_onModelEvent","_removeReference");this._reset();a&&this.reset(a,{silent:!0});this.initialize.apply(this,arguments)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c').hide().appendTo("body")[0].contentWindow,this.navigate(a); -this._hasPushState?g(window).bind("popstate",this.checkUrl):"onhashchange"in window&&!b?g(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);this.fragment=a;m=!0;a=window.location;b=a.pathname==this.options.root;if(this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;else if(this._wantsPushState&&this._hasPushState&&b&&a.hash)this.fragment=a.hash.replace(j,""),window.history.replaceState({}, -document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);if(!this.options.silent)return this.loadUrl()},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b=this.fragment=this.getFragment(a); -return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){var c=(a||"").replace(j,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c))){if(this._hasPushState){var d=window.location;c.indexOf(this.options.root)!=0&&(c=this.options.root+c);this.fragment=c;window.history.pushState({},document.title,d.protocol+"//"+d.host+c)}else if(window.location.hash=this.fragment=c,this.iframe&&c!=this.getFragment(this.iframe.location.hash))this.iframe.document.open().close(), -this.iframe.location.hash=c;b&&this.loadUrl(a)}}});e.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize.apply(this,arguments)};var u=/^(\S+)\s*(.*)$/,n=["model","collection","el","id","attributes","className","tagName"];f.extend(e.View.prototype,e.Events,{tagName:"div",$:function(a){return g(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){g(this.el).remove();return this},make:function(a, -b,c){a=document.createElement(a);b&&g(a).attr(b);c&&g(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events))for(var b in f.isFunction(a)&&(a=a.call(this)),g(this.el).unbind(".delegateEvents"+this.cid),a){var c=this[a[b]];if(!c)throw Error('Event "'+a[b]+'" does not exist');var d=b.match(u),e=d[1];d=d[2];c=f.bind(c,this);e+=".delegateEvents"+this.cid;d===""?g(this.el).bind(e,c):g(this.el).delegate(d,e,c)}},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b= -0,c=n.length;b/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); + }, has(a) { + return this.attributes[a] != null; + }, set(a, b) { + b || (b = {}); if (!a) { + return this; + } + + if (a.attributes) { + a = a.attributes; + } + + const c = this.attributes; const d = this._escapedAttributes; if (!b.silent && this.validate && !this._performValidation(a, b)) { + return !1; + } + + if (this.idAttribute in a) { + this.id = a[this.idAttribute]; + } + + const e = this._changing; this._changing = !0; for (const g in a) { + const h = a[g]; if (!f.isEqual(c[g], h)) { + c[g] = h, delete d[g], this._changed = !0, b.silent || this.trigger('change:' + g, this, h, b); + } + } + + !e && !b.silent && this._changed && this.change(b); this._changing = !1; return this; + }, unset(a, b) { + if (!(a in this.attributes)) { + return this; + } + + b || (b = {}); const c = {}; c[a] = void 0; if (!b.silent && this.validate && !this._performValidation(c, b)) { + return !1; + } + + delete this.attributes[a]; delete this._escapedAttributes[a]; a == this.idAttribute && delete this.id; this._changed = +!0; b.silent || (this.trigger('change:' + a, this, void 0, b), this.change(b)); return this; + }, clear(a) { + a || (a = {}); let b; const c = this.attributes; const d = {}; for (b in c) { + d[b] = void 0; + } + + if (!a.silent && this.validate && !this._performValidation(d, a)) { + return !1; + } + + this.attributes = {}; this._escapedAttributes = {}; this._changed = !0; if (!a.silent) { + for (b in c) { + this.trigger('change:' + b, this, void 0, a); + } + + this.change(a); + } + + return this; + }, fetch(a) { + a || (a = {}); const b = this; const c = a.success; a.success = function (d, e, f) { + if (!b.set(b.parse(d, f), a)) { + return !1; + } + + c && +c(b, d); + }; + + a.error = i(a.error, b, a); return (this.sync || e.sync).call(this, 'read', this, a); + }, save(a, b) { + b || (b = {}); if (a && !this.set(a, b)) { + return !1; + } + + const c = this; const d = b.success; b.success = function (a, e, f) { + if (!c.set(c.parse(a, f), b)) { + return !1; + } + + d && d(c, a, f); + }; + + b.error = i(b.error, c, b); const f = this.isNew() ? 'create' : 'update'; return (this.sync || e.sync).call(this, f, this, b); + }, destroy(a) { + a || (a = {}); if (this.isNew()) { + return this.trigger('destroy', this, this.collection, a); + } + + const b = this; const c = a.success; a.success = function (d) { + b.trigger('destroy', + b, b.collection, a); c && c(b, d); + }; + + a.error = i(a.error, b, a); return (this.sync || e.sync).call(this, 'delete', this, a); + }, url() { + const a = k(this.collection) || this.urlRoot || l(); if (this.isNew()) { + return a; + } + + return a + (a.charAt(a.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); + }, parse(a) { + return a; + }, clone() { + return new this.constructor(this); + }, isNew() { + return this.id == null; + }, change(a) { + this.trigger('change', this, a); this._previousAttributes = f.clone(this.attributes); this._changed = !1; + }, hasChanged(a) { + if (a) { + return this._previousAttributes[a] != +this.attributes[a]; + } + + return this._changed; + }, changedAttributes(a) { + a || (a = this.attributes); const b = this._previousAttributes; let c = !1; let d; for (d in a) { + f.isEqual(b[d], a[d]) || (c = c || {}, c[d] = a[d]); + } + + return c; + }, previous(a) { + if (!a || !this._previousAttributes) { + return null; + } + + return this._previousAttributes[a]; + }, previousAttributes() { + return f.clone(this._previousAttributes); + }, _performValidation(a, b) { + const c = this.validate(a); if (c) { + return b.error ? b.error(this, c, b) : this.trigger('error', this, c, b), !1; + } + + return !0; + }}); + e.Collection = function (a, b) { + b || (b = {}); if (b.comparator) { + this.comparator = b.comparator; + } + + f.bindAll(this, '_onModelEvent', '_removeReference'); this._reset(); a && this.reset(a, {silent: !0}); Reflect.apply(this.initialize, this, arguments); + }; + + f.extend(e.Collection.prototype, e.Events, {model: e.Model, initialize() {}, toJSON() { + return this.map(a => { + return a.toJSON(); + }); + }, add(a, b) { + if (f.isArray(a)) { + for (let c = 0, d = a.length; c < d; c++) { + this._add(a[c], b); + } + } else { + this._add(a, b); + } + + return this; + }, remove(a, b) { + if (f.isArray(a)) { + for (let c = +0, d = a.length; c < d; c++) { + this._remove(a[c], b); + } + } else { + this._remove(a, b); + } + + return this; + }, get(a) { + if (a == null) { + return null; + } + + return this._byId[a.id != null ? a.id : a]; + }, getByCid(a) { + return a && this._byCid[a.cid || a]; + }, at(a) { + return this.models[a]; + }, sort(a) { + a || (a = {}); if (!this.comparator) { + throw new Error('Cannot sort a set without a comparator'); + } + + this.models = this.sortBy(this.comparator); a.silent || this.trigger('reset', this, a); return this; + }, pluck(a) { + return f.map(this.models, b => { + return b.get(a); + }); + }, + reset(a, b) { + a || (a = []); b || (b = {}); this.each(this._removeReference); this._reset(); this.add(a, {silent: !0}); b.silent || this.trigger('reset', this, b); return this; + }, fetch(a) { + a || (a = {}); const b = this; const c = a.success; a.success = function (d, f, e) { + b[a.add ? 'add' : 'reset'](b.parse(d, e), a); c && c(b, d); + }; + + a.error = i(a.error, b, a); return (this.sync || e.sync).call(this, 'read', this, a); + }, create(a, b) { + const c = this; b || (b = {}); a = this._prepareModel(a, b); if (!a) { + return !1; + } + + const d = b.success; b.success = function (a, e, f) { + c.add(a, b); + d && d(a, e, f); + }; + + a.save(null, b); return a; + }, parse(a) { + return a; + }, chain() { + return f(this.models).chain(); + }, _reset() { + this.length = 0; this.models = []; this._byId = {}; this._byCid = {}; + }, _prepareModel(a, b) { + if (a instanceof e.Model) { + if (!a.collection) { + a.collection = this; + } + } else { + const c = a; a = new this.model(c, {collection: this}); a.validate && !a._performValidation(c, b) && (a = !1); + } + + return a; + }, _add(a, b) { + b || (b = {}); a = this._prepareModel(a, b); if (!a) { + return !1; + } + + const c = this.getByCid(a); if (c) { + throw new Error(['Can\'t add the same model to a set twice', + c.id]); + } + + this._byId[a.id] = a; this._byCid[a.cid] = a; this.models.splice(b.at != null ? b.at : (this.comparator ? this.sortedIndex(a, this.comparator) : this.length), 0, a); a.bind('all', this._onModelEvent); this.length++; b.silent || a.trigger('add', a, this, b); return a; + }, _remove(a, b) { + b || (b = {}); a = this.getByCid(a) || this.get(a); if (!a) { + return null; + } + + delete this._byId[a.id]; delete this._byCid[a.cid]; this.models.splice(this.indexOf(a), 1); this.length--; b.silent || a.trigger('remove', a, this, b); this._removeReference(a); return a; + }, + _removeReference(a) { + this == a.collection && delete a.collection; a.unbind('all', this._onModelEvent); + }, _onModelEvent(a, b, c, d) { + (a == 'add' || a == 'remove') && c != this || (a == 'destroy' && this._remove(b, d), b && a === 'change:' + b.idAttribute && (delete this._byId[b.previous(b.idAttribute)], this._byId[b.id] = b), Reflect.apply(this.trigger, this, arguments)); + }}); f.each(['forEach', + 'each', + 'map', + 'reduce', + 'reduceRight', + 'find', + 'detect', + 'filter', + 'select', + 'reject', + 'every', + 'all', + 'some', + 'any', + 'include', + 'contains', + 'invoke', + 'max', + 'min', + 'sortBy', + 'sortedIndex', + 'toArray', + 'size', + 'first', + 'rest', + 'last', + 'without', + 'indexOf', + 'lastIndexOf', + 'isEmpty', + 'groupBy'], a => { + e.Collection.prototype[a] = function () { + return f[a].apply(f, [this.models].concat(f.toArray(arguments))); + }; + }); e.Router = function (a) { + a || (a = {}); if (a.routes) { + this.routes = a.routes; + } + + this._bindRoutes(); Reflect.apply(this.initialize, this, arguments); + }; + + const q = /:([\w\d]+)/g; const r = /\*([\w\d]+)/g; const s = /[-[\]{}()+?.,\\^$|#\s]/g; f.extend(e.Router.prototype, e.Events, {initialize() {}, route(a, + b, c) { + e.history || (e.history = new e.History()); f.isRegExp(a) || (a = this._routeToRegExp(a)); e.history.route(a, f.bind(function (d) { + d = this._extractParameters(a, d); c.apply(this, d); this.trigger.apply(this, ['route:' + b].concat(d)); + }, this)); + }, navigate(a, b) { + e.history.navigate(a, b); + }, _bindRoutes() { + if (this.routes) { + const a = []; let b; for (b in this.routes) { + a.unshift([b, this.routes[b]]); + } + + b = 0; for (let c = a.length; b < c; b++) { + this.route(a[b][0], a[b][1], this[a[b][1]]); + } + } + }, _routeToRegExp(a) { + a = a.replace(s, '\\$&').replace(q, + '([^/]*)').replace(r, '(.*?)'); return new RegExp('^' + a + '$'); + }, _extractParameters(a, b) { + return a.exec(b).slice(1); + }}); e.History = function () { + this.handlers = []; f.bindAll(this, 'checkUrl'); + }; + + const j = /^#*/; const t = /msie [\w.]+/; let m = !1; f.extend(e.History.prototype, {interval: 50, getFragment(a, b) { + if (a == null) { + if (this._hasPushState || b) { + a = window.location.pathname; const c = window.location.search; c && (a += c); a.indexOf(this.options.root) == 0 && (a = a.slice(this.options.root.length)); + } else { + a = window.location.hash; + } + } + + return decodeURIComponent(a.replace(j, + '')); + }, start(a) { + if (m) { + throw new Error('Backbone.history has already been started'); + } + + this.options = f.extend({}, {root: '/'}, this.options, a); this._wantsPushState = Boolean(this.options.pushState); this._hasPushState = !(!this.options.pushState || !window.history || !window.history.pushState); a = this.getFragment(); let b = document.documentMode; if (b = t.exec(navigator.userAgent.toLowerCase()) && (!b || b <= 7)) { + this.iframe = g('