Full Code of visionmedia/page.js for AI

master 4f9991658f9b cached
58 files
173.5 KB
48.1k tokens
85 symbols
1 requests
Download .txt
Repository: visionmedia/page.js
Branch: master
Commit: 4f9991658f9b
Files: 58
Total size: 173.5 KB

Directory structure:
gitextract_171_6o_z/

├── .gitignore
├── .jshintrc
├── .travis.yml
├── History.md
├── Makefile
├── Readme.md
├── bower.json
├── component.json
├── examples/
│   ├── album/
│   │   ├── app.js
│   │   ├── index.html
│   │   └── style.css
│   ├── basic/
│   │   └── index.html
│   ├── chrome/
│   │   ├── chrome.css
│   │   └── index.html
│   ├── enterprisejs/
│   │   └── index.html
│   ├── hash/
│   │   └── index.html
│   ├── hashbang/
│   │   └── index.html
│   ├── index.js
│   ├── list.jade
│   ├── notfound/
│   │   └── index.html
│   ├── partials/
│   │   ├── app.js
│   │   ├── index.html
│   │   ├── public/
│   │   │   ├── css/
│   │   │   │   └── style.css
│   │   │   └── js/
│   │   │       └── events.js
│   │   ├── routes/
│   │   │   └── index.js
│   │   ├── test/
│   │   │   ├── index.html
│   │   │   └── tests.js
│   │   └── views/
│   │       ├── about.html
│   │       ├── content.html
│   │       ├── home.html
│   │       └── portfolio.html
│   ├── profile/
│   │   ├── app.js
│   │   ├── index.html
│   │   └── style.css
│   ├── query-string/
│   │   ├── app.js
│   │   ├── index.html
│   │   ├── query.js
│   │   └── style.css
│   ├── server/
│   │   ├── app.js
│   │   ├── index.html
│   │   └── style.css
│   ├── state/
│   │   ├── app.js
│   │   ├── index.html
│   │   └── style.css
│   └── transitions/
│       ├── app.js
│       ├── index.html
│       └── style.css
├── index.js
├── package.json
├── page.js
├── page.mjs
├── rollup.config.js
└── test/
    ├── demos/
    │   └── back-demo.html
    ├── index.html
    ├── mocha.opts
    ├── support/
    │   └── jsdom.js
    ├── test-page.html
    └── tests.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules
testing
.jshint
coverage.html
npm-debug.log
index-cov.js
.DS_Store
package-lock.json


================================================
FILE: .jshintrc
================================================
{
  "browser": true,
  "node":true,
  "expr": true,
  "eqnull": true,
  "laxcomma": true,
  "-W079": true,
  "-W014": true,
  "curly": false,
  "eqeqeq": true,
  "immed": true,
  "latedef": true,
  "newcap": true,
  "noarg": true,
  "quotmark": "single",
  "regexp": true,
  "undef": true,
  "unused": false,
  "strict": true,
  "trailing": false,
  "smarttabs": true,
  "latedef": false,
  "indent": 2,
  "validthis": true
}


================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
  - 0.10
  - node
before_script:
  - npm run engine-deps
after_success:
  - ./node_modules/.bin/jscoverage index.js index-cov.js
  - PAGE_COV=1 ./node_modules/.bin/mocha test/tests.js -R mocha-lcov-reporter | ./node_modules/coveralls/bin/coveralls.js
sudo: false


================================================
FILE: History.md
================================================
1.8.3 / 2018-01-22
==================

This is a patch release which switches the build to rollup. Switching shaves 2k off of the gzipped size. In addition to this benefit, this also fixes a couple of small issues:

## Issues

* [#444] - Closure compiler warnings
* [#458] - unsafe-eval error

[#444]: https://github.com/visionmedia/page.js/issues/444
[#458]: https://github.com/visionmedia/page.js/issues/458

1.8.2 / 2018-01-22
==================

This is a patch release fixing an issue that was meant to be solved in 1.8.1. page.js now runs in Node.js again, when there isn't a window environment.

1.8.1 / 2018-01-22
==================

This is a patch release, fixing an issue with Node.js usage.

1.8.0 / 2018-01-17
==================

This is a minor release, adding one new (minor) feature and a few bug fixes and documentation improvements.

## Controlling other pages

The new feature of this release is that page.js can now control other pages (windows) besides the main window. This makes it possible to control an iframe's routing. You can use it by passing the window option to start:

    page('/', function(){
      // Whoa, I'm inside an iframe!
    });

page.start({ window: myiFrame.contentWindow });

Note that page is still a singleton, which means you can only have 1 page, so you can't control both an iframe and the main window at the same time.

This change was made to improve our testing, but in the future we'll make it possible to have multiple page objects so you really will be able to control multiple iframes and the main window all at the same time.

## Better hashbang support

Hashbang support has never been very good in page.js. But we're slowly improving it! In 1.8.0 we've fixed the problem where the app's full URL would show up in the hashbang upon start. http://example.com/my/page#!/my/page. Gross! No longer happens thanks to #447.

## Pull Requests

Those are the big things, but here's a list of all of the pull requests merged into 1.8.0:

* [#445] - Prevent hash from being part of the ctx.pathname
* [#447] - Prevent going to the root when setting up the initial hashchange route
* [#446] - Add a note about usage with a CDN
* [#443] - Change test infrastructure to run inside of iframes
* [#303] - page.exit callback example missing context param
* [#267] - Added clarification for decoded plus signs in urls

[#445]: https://github.com/visionmedia/page.js/pull/4445
[#447]: https://github.com/visionmedia/page.js/pull/4447
[#446]: https://github.com/visionmedia/page.js/pull/4446
[#443]: https://github.com/visionmedia/page.js/pull/4443
[#303]: https://github.com/visionmedia/page.js/pull/4303
[#267]: https://github.com/visionmedia/page.js/pull/4267

1.7.3 / 2018-01-15
==================

This is a patch release making an improvement to how page.js works on pages that use the file protocol, such as Electron and nw.js apps.

## Pull Requests

* [#441] - Set the page's base to be the current location when used under the file protocol with hashbang routing.

[#441]: https://github.com/visionmedia/page.js/pull/441

1.7.2 / 2018-01-15
==================

Our first release in almost 2 years! This is a long overdue patch release that contains various bug fixes that have taken place over the course of the last couple of years. As releases will become much more often in the future (containing only a few fixes in most cases), I will be listing the closed issues in releases, but because there are 2 years worth it is not practical to do so in this release.

While you're here, if you haven't checked out page.js in a long time now is a great time. I've recently taken over maintenance and have a plan in place to take this great small library into the future. For now I am concentrating on stabilizing 1.x by fixing as many of the backlog of issues that have built up as I can. Once that is complete we'll start thinking about 2.0.

If you've submitted a PR here in the past and seen it be ignored, please come back! Your contributions are invaluable, and I promise that as long as I'm maintaining this project I'll do my best to always at least comment on pull requests and try to get them resolved.

That's all for now! Please report any issues you find in 1.7.2.

1.7.1 / 2016-03-17
==================

* [#363] - Reinstate shadow DOM fixes which were reverted

[#363]: https://github.com/visionmedia/page.js/issues/363

1.7.0 / 2016-03-12
==================

* [#284] - Use shadow dom when available ([@mwalid])
* [#329] - Add type annotations for full closure-compiler advanced optimization support ([@chadkillingsworth])
* [#328] - Include ctx in page.after example, fixes [#290]() ([@aaronshaf])

[#284]: https://github.com/visionmedia/page.js/issues/284
[#329]: https://github.com/visionmedia/page.js/issues/329
[#328]: https://github.com/visionmedia/page.js/issues/328
[#290]: https://github.com/visionmedia/page.js/issues/290
[@mwalid]: https://github.com/mwalid
[@chadkillingsworth]: https://github.com/chadkillingsworth
[@aaronshaf]: https://github.com/aaronshaf

1.6.4 / 2015-10-09
==================

* fix wildcard route support (update path-to-regexp to v1.2.1)

1.6.3 / 2015-04-19
==================

* fix including page.js on server side
* fix including page.js if the document is already loaded
* fix bug with click-event in Firefox

1.6.2 / 2015-03-06
==================

* fix touch support #236
* fix nw.js support #238
* fix popstate issue in Safari #213

1.6.1 / 2015-02-16
==================

* added `page.js` to npm files
* back button works correct with hash links in Firefox  #218

1.6.0./ 2015-01-27
==================

* added `page.back` feature #157
* added `decodeURLComponents` option #187
* now `ctx.params` is object like in express #203
* skip route processing if another one is called #172
* docs improved
* tests improved


1.5.0 / 2014-11-29
==================

* added page.exit(path, callback[, callback...])
* added page.redirect(url)
* fix: ignore links with `download` attribute
* fix: remove URL encoding before parsing paths

1.4.1 / 2014-11-14
==================

* fixed: hashbang navigation
* added hashbang example
* added tests

1.4.0 / 2014-11-12
==================

 * add hashbang support. Closes #112
 * add page.redirect() method
 * add plugins list to readme
 * add Context#handled option
 * Fix an issue where redirects in dispatch can be overwritten by ctx.save()
 * add support HTML5-History-API polyfill
 * make sameOrigin public
 * update path-to-regexp
 * allow for missing href in anchors.
 * update examples


1.3.7 / 2013-09-09
==================

 * fix removal of fragment

1.3.6 / 2013-03-12
==================

  * fix links with target attribute

1.3.5 / 2013-02-12
==================

  * fix ctrl/cmd/shift clicks

1.3.4 / 2013-02-04
==================

  * add tmp .show() dispatch argument
  * add keywords to component.json

1.3.3 / 2012-12-14
==================

  * remove + support from path regexps

1.3.2 / 2012-11-26
==================

  * add explicit "#" check
  * add `window` to `addEventListener` calls

1.3.1 / 2012-09-21
==================

  * fix: onclick only when e.which == 1

1.3.0 / 2012-08-29
==================

  * add `page(fn)` support. Closes #27
  * add component.json
  * fix tests
  * fix examples

1.2.1 / 2012-08-02
==================

  * add transitions example
  * add exposing of `Context` and `Route` constructors
  * fix infinite loop issue unhandled paths containing query-strings

1.2.0 / 2012-07-05
==================

  * add `ctx.pathname`
  * add `ctx.querystring`
  * add support for passing a query-string through the dispatcher [ovaillancourt]
  * add `.defaultPrevented` support, ignoring page.js handling [ovaillancourt]

1.1.3 / 2012-06-18
==================

  * Added some basic client-side tests
  * Fixed initial dispatch in Firefox
  * Changed: no-op on subsequent `page()` calls. Closes #16

1.1.2 / 2012-06-13
==================

  * Fixed origin portno bug preventing :80 and :443 from working properly
  * Fixed: prevent cyclic refreshes. Closes #17

1.1.1 / 2012-06-11
==================

  * Added enterprisejs example
  * Added: join base for `.canonicalPath`. Closes #12
  * Fixed `location.origin` usage [fisch42]
  * Fixed `pushState()` when unhandled

1.1.0 / 2012-06-06
==================

  * Added `+` support to pathtoRegexp()
  * Added `page.base(path)` support
  * Added dispatch option to `page()`. Closes #10
  * Added `Context#originalPath`
  * Fixed unhandled links when .base is present. Closes #11
  * Fixed: `Context#path` to "/"

0.0.2 / 2012-06-05
==================

  * Added `make clean`
  * Added some mocha tests
  * Fixed: ignore fragments
  * Fixed: do not pushState on initial load


================================================
FILE: Makefile
================================================
ROLLUP=node_modules/.bin/rollup
INFOLOG := \033[34m ▸\033[0m

all: page.js page.mjs
.PHONY: all

page.js: index.js
	@echo "$(INFOLOG) Building page.js.."
	@$(ROLLUP) -c rollup.config.js

page.mjs: index.js
	@echo "$(INFOLOG) Building page.mjs.."
	@$(ROLLUP) -c rollup.config.js -f es -o $@

watch:
	find index.js | entr make page.js
.PHONY: watch

clean:
	@rm page.js page.mjs
.PHONY: clean


================================================
FILE: Readme.md
================================================
 ![page router logo](http://f.cl.ly/items/3i3n001d0s1Q031r2q1P/page.png)

Tiny Express-inspired client-side router.

 [![Build Status](https://travis-ci.org/visionmedia/page.js.svg?branch=master)](https://travis-ci.org/visionmedia/page.js)
[![Coverage Status](https://coveralls.io/repos/visionmedia/page.js/badge.svg?branch=master)](https://coveralls.io/r/visionmedia/page.js?branch=master)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/visionmedia/page.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)

```js
page('/', index)
page('/user/:user', show)
page('/user/:user/edit', edit)
page('/user/:user/album', album)
page('/user/:user/album/sort', sort)
page('*', notfound)
page()
```

## Installation

  There are multiple ways to install `page.js`.
  With package managers:

  ```bash
  $ npm install page # for browserify
  $ component install visionmedia/page.js
  $ bower install visionmedia/page.js
  ```

  Or use with a CDN. We support:

  * [cdnjs](https://cdnjs.com/libraries/page.js)
  * [unpkg](https://unpkg.com/page/page.js)

  Using with global script tags:

  ```html
  <script src="https://unpkg.com/page/page.js"></script>
  <script>
    page('/about', function(){
      // Do stuff
    });
  </script>
  ```

  Or with modules, in modern browsers:

  ```html
  <script type="module">
    import page from "//unpkg.com/page/page.mjs";

    page('/home', () => { ... });
  </script>
  ```

## Running examples

  To run examples do the following to install dev dependencies and run the example server:

    $ git clone git://github.com/visionmedia/page.js
    $ cd page.js
    $ npm install
    $ node examples
    $ open http://localhost:4000

 Currently we have examples for:

   - `basic` minimal application showing basic routing
   - `notfound` similar to `basic` with single-page 404 support
   - `album` showing pagination and external links
   - `profile` simple user profiles
   - `query-string` shows how you can integrate plugins using the router
   - `state` illustrates how the history state may be used to cache data
   - `server` illustrates how to use the dispatch option to server initial content
   - `chrome` Google Chrome style administration interface
   - `transitions` Shows off a simple technique for adding transitions between "pages"
   - `partials` using hogan.js to render mustache partials client side

  __NOTE__: keep in mind these examples do not use jQuery or similar, so
  portions of the examples may be relatively verbose, though they're not
  directly related to page.js in any way.

## API

### page(path, callback[, callback ...])

  Defines a route mapping `path` to the given `callback(s)`.
  Each callback is invoked with two arguments, [context](#context) and `next`. Much like Express invoking next will call the next registered callback with the given path.

```js
page('/', user.list)
page('/user/:id', user.load, user.show)
page('/user/:id/edit', user.load, user.edit)
page('*', notfound)
```

  Under certain conditions, links will be disregarded
  and will not be dispatched, such as:

  - Links that are not of the same origin
  - Links with the `download` attribute
  - Links with the `target` attribute
  - Links with the `rel="external"` attribute

### page(callback)

  This is equivalent to `page('*', callback)` for generic "middleware".

### page(path)

  Navigate to the given `path`.

```js
$('.view').click(function(e){
  page('/user/12')
  e.preventDefault()
})
```

### page(fromPath, toPath)

  Setup redirect from one path to another.

### page.redirect(fromPath, toPath)

  Identical to `page(fromPath, toPath)`

### page.redirect(path)
  Calling page.redirect with only a string as the first parameter
  redirects to another route.
  Waits for the current route to push state and after replaces it
  with the new one leaving the browser history clean.

```js
page('/default', function(){
  // some logic to decide which route to redirect to
  if(admin) {
    page.redirect('/admin');
  } else {
    page.redirect('/guest');
  }
});

page('/default');
```

### page.show(path)

  Identical to `page(path)` above.

### page([options])

  Register page's `popstate` / `click` bindings. If you're
  doing selective binding you'll like want to pass `{ click: false }`
  to specify this yourself. The following options are available:

  - `click` bind to click events [__true__]
  - `popstate` bind to popstate [__true__]
  - `dispatch` perform initial dispatch [__true__]
  - `hashbang` add `#!` before urls [__false__]
  - `decodeURLComponents` remove URL encoding from path components (query string, pathname, hash) [__true__]
  - `window` provide a window to control (by default it will control the main window)

  If you wish to load serve initial content
  from the server you likely will want to
  set `dispatch` to __false__.

### page.start([options])

  Identical to `page([options])` above.

### page.stop()

  Unbind both the `popstate` and `click` handlers.

### page.base([path])

  Get or set the base `path`. For example if page.js
  is operating within `/blog/*` set the base path to "/blog".

### page.strict([enable])

  Get or set the strict path matching mode to `enable`. If enabled
  `/blog` will not match "/blog/" and `/blog/` will not match "/blog".

### page.exit(path, callback[, callback ...])

  Defines an exit route mapping `path` to the given `callback(s)`.

  Exit routes are called when a page changes, using the context
  from the previous change. For example:

```js
page('/sidebar', function(ctx, next) {
  sidebar.open = true
  next()
})

page.exit('/sidebar', function(ctx, next) {
  sidebar.open = false
  next()
})
```

### page.exit(callback)

Equivalent to `page.exit('*', callback)`.

### page.create([options])

Create a new page instance with the given options. Options provided
are the same as provided in `page([options])` above. Use this if you need
to control multiple windows (like iframes or popups) in addition
to the main window.

```js
var otherPage = page.create({ window: iframe.contentWindow });
otherPage('/', main);
```

### page.clickHandler

This is the click handler used by page to handle routing when a user clicks an anchor like `<a href="/user/profile">`. This is exported for those who want to disable the click handling behavior with `page.start({ click: false })`, but still might want to dispatch based on the click handler's logic in some scenarios.

### Context

  Routes are passed `Context` objects, these may
  be used to share state, for example `ctx.user =`,
  as well as the history "state" `ctx.state` that
  the `pushState` API provides.

#### Context#save()

  Saves the context using `replaceState()`. For example
  this is useful for caching HTML or other resources
  that were loaded for when a user presses "back".

#### Context#handled

  If `true`, marks the context as handled to prevent [default 404 behaviour][404].
  For example this is useful for the routes with interminate quantity of the
  callbacks.

[404]: https://github.com/visionmedia/page.js#default-404-behaviour

#### Context#canonicalPath

  Pathname including the "base" (if any) and query string "/admin/login?foo=bar".

#### Context#path

  Pathname and query string "/login?foo=bar".

#### Context#querystring

  Query string void of leading `?` such as "foo=bar", defaults to "".

#### Context#pathname

  The pathname void of query string "/login".

#### Context#state

  The `pushState` state object.

#### Context#title

  The `pushState` title.

## Routing

  The router uses the same string-to-regexp conversion
  that Express does, so things like ":id", ":id?", and "*" work
  as you might expect.

  Another aspect that is much like Express is the ability to
  pass multiple callbacks. You can use this to your advantage
  to flatten nested callbacks, or simply to abstract components.

### Separating concerns

  For example suppose you have a route to _edit_ users, and a
  route to _view_ users. In both cases you need to load the user.
  One way to achieve this is with several callbacks as shown here:

```js
page('/user/:user', load, show)
page('/user/:user/edit', load, edit)
```

  Using the `*` character we can alter this to match all
  routes prefixed with "/user" to achieve the same result:

```js
page('/user/*', load)
page('/user/:user', show)
page('/user/:user/edit', edit)
```

  Likewise `*` can be used as catch-alls after all routes
  acting as a 404 handler, before all routes, in-between and
  so on. For example:

```js
page('/user/:user', load, show)
page('*', function(){
  $('body').text('Not found!')
})
```

### Default 404 behaviour

  By default when a route is not matched,
  page.js invokes `page.stop()` to unbind
  itself, and proceed with redirecting to the
  location requested. This means you may use
  page.js with a multi-page application _without_
  explicitly binding to certain links.

### Working with parameters and contexts

  Much like `request` and `response` objects are
  passed around in Express, page.js has a single
  "Context" object. Using the previous examples
  of `load` and `show` for a user, we can assign
  arbitrary properties to `ctx` to maintain state
  between callbacks.

  To build a `load` function that will load
  the user for subsequent routes you'll need to
  access the ":id" passed. You can do this with
  `ctx.params.NAME` much like Express:

```js
function load(ctx, next){
  var id = ctx.params.id
}
```

  Then perform some kind of action against the server,
  assigning the user to `ctx.user` for other routes to
  utilize. `next()` is then invoked to pass control to
  the following matching route in sequence, if any.

```js
function load(ctx, next){
  var id = ctx.params.id
  $.getJSON('/user/' + id + '.json', function(user){
    ctx.user = user
    next()
  })
}
```

  The "show" function might look something like this,
  however you may render templates or do anything you
  want. Note that here `next()` is _not_ invoked, because
  this is considered the "end point", and no routes
  will be matched until another link is clicked or
  `page(path)` is called.

```js
function show(ctx){
  $('body')
    .empty()
    .append('<h1>' + ctx.user.name + '<h1>');
}
```

  Finally using them like so:

```js
page('/user/:id', load, show)
```

**NOTE:** The value of `ctx.params.NAME` is decoded via `decodeURIComponent(sliceOfUrl)`. One exception though is the use of the plus sign (+) in the url, e.g. `/user/john+doe`, which is decoded to a space: `ctx.params.id == 'john doe'`. Also an encoded plus sign (`%2B`) is decoded to a space.

### Working with state

  When working with the `pushState` API,
  and page.js you may optionally provide
  state objects available when the user navigates
  the history.

  For example if you had a photo application
  and you performed a relatively extensive
  search to populate a list of images,
  normally when a user clicks "back" in
  the browser the route would be invoked
  and the query would be made yet-again.

  An example implementation might look as follows:

```js
function show(ctx){
  $.getJSON('/photos', function(images){
    displayImages(images)
  })
}
```

   You may utilize the history's state
   object to cache this result, or any
   other values you wish. This makes it
   possible to completely omit the query
   when a user presses back, providing
   a much nicer experience.

```js
function show(ctx){
  if (ctx.state.images) {
    displayImages(ctx.state.images)
  } else {
    $.getJSON('/photos', function(images){
      ctx.state.images = images
      ctx.save()
      displayImages(images)
    })
  }
}
```

  __NOTE__: `ctx.save()` must be used
  if the state changes _after_ the first
  tick (xhr, setTimeout, etc), otherwise
  it is optional and the state will be
  saved after dispatching.

### Matching paths

  Here are some examples of what's possible
  with the string to `RegExp` conversion.

  Match an explicit path:

```js
page('/about', callback)
```

  Match with required parameter accessed via `ctx.params.name`:

```js
page('/user/:name', callback)
```

  Match with several params, for example `/user/tj/edit` or
  `/user/tj/view`.

```js
page('/user/:name/:operation', callback)
```

  Match with one optional and one required, now `/user/tj`
  will match the same route as `/user/tj/show` etc:

```js
page('/user/:name/:operation?', callback)
```

  Use the wildcard char `*` to match across segments,
  available via `ctx.params[N]` where __N__ is the
  index of `*` since you may use several. For example
  the following will match `/user/12/edit`, `/user/12/albums/2/admin`
  and so on.

```js
page('/user/*', loadUser)
```

  Named wildcard accessed, for example `/file/javascripts/jquery.js`
  would provide "/javascripts/jquery.js" as `ctx.params.file`:

```js
page('/file/:file(.*)', loadUser)
```

  And of course `RegExp` literals, where the capture
  groups are available via `ctx.params[N]` where __N__
  is the index of the capture group.

```js
page(/^\/commits\/(\d+)\.\.(\d+)/, loadUser)
```

## Plugins

  An example plugin _examples/query-string/query.js_
  demonstrates how to make plugins. It will provide a parsed `ctx.query` object
  derived from [node-querystring](https://github.com/visionmedia/node-querystring).

  Usage by using "*" to match any path
  in order to parse the query-string:

```js
page('*', parse)
page('/', show)
page()

function parse(ctx, next) {
  ctx.query = qs.parse(location.search.slice(1));
  next();
}

function show(ctx) {
  if (Object.keys(ctx.query).length) {
    document
      .querySelector('pre')
      .textContent = JSON.stringify(ctx.query, null, 2);
  }
}
```

### Available plugins

- [querystring](https://github.com/visionmedia/page.js/blob/master/examples/query-string/query.js): provides a parsed `ctx.query` object derived from [node-querystring](https://github.com/visionmedia/node-querystring).
- [body-parser](https://github.com/kethinov/page.js-body-parser.js): provides a `req.body` object for routes derived from [body-parser](https://github.com/expressjs/body-parser).
- [express-mapper](https://github.com/kethinov/page.js-express-mapper.js): provides a direct imitation of the [Express](http://expressjs.com/) API so you can share controller code on the client and the server with your Express application without modification.

Please submit pull requests to add more to this list.

### Running tests

In the console:

```
$ npm install
$ npm test
```

In the browser:

```
$ npm install
$ npm run serve
$ open http://localhost:3000/
```

### Support in IE8+

If you want the router to work in older version of Internet Explorer that don't support pushState, you can use the [HTML5-History-API](https://github.com/devote/HTML5-History-API) polyfill:
```bash
  npm install html5-history-api
```

##### How to use a Polyfill together with router (OPTIONAL):
If your web app is located within a nested basepath, you will need to specify the `basepath` for the HTML5-History-API polyfill.
Before calling `page.base()` use: `history.redirect([prefixType], [basepath])` - Translation link if required.
  * `prefixType`: `[string|null]` - Substitute the string after the anchor (#) by default "/".
  * `basepath`: `[string|null]` - Set the base path. See `page.base()` by default "/". (Note: Slash after `pathname` required)

### Pull Requests

  * Break commits into a single objective.
  * An objective should be a chunk of code that is related but requires explanation.
  * Commits should be in the form of what-it-is: how-it-does-it and or why-it's-needed or what-it-is for trivial changes
  * Pull requests and commits should be a guide to the code.

## Server configuration

  In order to load and update any URL managed by page.js, you need to configure your environment to point to your project's main file (index.html, for example) for each non-existent URL. Below you will find examples for most common server scenarios.

### Nginx

If using Nginx, add this to the .conf file related to your project (inside the "server" part), and **reload** your Nginx server:

```nginx
location / {
    try_files $uri $uri/ /index.html?$args;
}
```

### Apache

If using Apache, create (or add to) the `.htaccess` file in the root of your public folder, with the code:

```apache
Options +FollowSymLinks
RewriteEngine On

RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f

RewriteRule ^.*$ ./index.html
```

### Node.js - Express

For development and/or production, using **Express**, you need to use `express-history-api-fallback` package. An example:

```js
import { join } from 'path';
import express from 'express';
import history from 'express-history-api-fallback';

const app = express();
const root = join(__dirname, '../public');

app.use(express.static(root));
app.use(history('index.html', { root }));

const server = app.listen(process.env.PORT || 3000);

export default server;
```

### Node.js - Browsersync

For development using **Browsersync**, you need to use `history-api-fallback` package. An example:

```js
var browserSync = require("browser-sync").create();
var historyApiFallback = require('connect-history-api-fallback');

browserSync.init({
	files: ["*.html", "css/*.css", "js/*.js"],
	server: {
		baseDir: ".",
		middleware: [ historyApiFallback() ]
	},
	port: 3030
});
```

## Integrations

## License

(The MIT License)

Copyright (c) 2012 TJ Holowaychuk &lt;tj@vision-media.ca&gt;

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: bower.json
================================================
{
  "name": "page",
  "description": "Tiny client-side router",
  "keywords": ["page", "route", "router", "routes", "pushState"],
  "main": "page.js",
  "repository": {
    "type": "git",
    "url": "git://github.com/visionmedia/page.js"
  },
  "ignore": [
    ".gitignore",
    ".npmignore",
    ".travis.yml",
    "component.json",
    "examples",
    "History.md",
    "Makefile",
    "package.json",
    "Readme.md",
    "test"
  ],
  "license": "MIT"
}


================================================
FILE: component.json
================================================
{
  "name": "page",
  "repo": "visionmedia/page.js",
  "version": "1.6.4",
  "description": "Tiny client-side router",
  "keywords": ["page", "route", "router", "routes", "pushState"],
  "scripts": ["index.js"],
  "license": "MIT",
  "dependencies": {
    "pillarjs/path-to-regexp": "v1.2.1"
  }
}


================================================
FILE: examples/album/app.js
================================================
var perPage = 6
  , prev = document.querySelector('#prev')
  , next = document.querySelector('#next');

page.base('/album');
page('/', '/photos/0');
page('/photos/:page', photos)
page('*', notfound);
page();

function photos(ctx) {
  var page = ~~ctx.params.page;
  var from = page * perPage;
  var to = from + perPage;
  console.log('showing page %s : %s..%s', page, from, to);
  var photos = images.slice(from, to);
  display(photos);
  adjustPager(page);
}

function notfound() {
  document.querySelector('p')
    .textContent = 'not found';
}

function display(photos) {
  var el = document.querySelector('#photos');
  el.innerHTML = '';
  photos.forEach(function(photo){
    var img = document.createElement('img');
    img.src = photo;
    el.appendChild(img);
  });
}

function adjustPager(page) {
  if (page) {
    prev.style.display = 'inline-block';
    prev.setAttribute('href', '/album/photos/' + (page - 1));
  } else {
    prev.style.display = 'none';
  }

  next.setAttribute('href', '/album/photos/' + (page + 1));
}

var images = [
    'http://upload.wikimedia.org/wikipedia/en/7/76/Grim_Fandango_artwork.jpg'
  , 'http://www.xblafans.com/wp-content/uploads//2011/08/Grim-Fandango1.jpg'
  , 'http://media.giantbomb.com/uploads/0/1371/190604-grimfandango106_super.jpg'
  , 'http://gamejunkienz.files.wordpress.com/2012/02/grimfandango.jpg'
  , 'http://onlyhdwallpapers.com/wallpaper/video_games_grim_fandango_lucas_arts_desktop_1024x768_wallpaper-305343.jpg'
  , 'http://lparchive.org/Grim-Fandango-(Screenshot)/Update%207/02176.gif'
  , 'http://bulk2.destructoid.com/ul/128679-GrimFandangoActionFigures.jpg'
  , 'http://www.gamasutra.com/features/20061103/grimfandango02.jpg'
  , 'http://metavideogame.files.wordpress.com/2011/05/grimhof_03_1081459316.jpg'
  , 'http://3.bp.blogspot.com/_zBnIHQUy4r4/SpxdDw1Z8tI/AAAAAAAABJM/FoCWfc8imnc/s400/GrimFandango1024x768.jpg'
  , 'http://www.deviantart.com/download/184571597/grim_fandango_by_domigorgon-d31w0ct.jpg'
  , 'http://vgboxart.com/boxes/PC/29535-grim-fandango.png?t=1243105773'
  , 'http://kastatic.com/i2/games/1/3/13230/10.png'
  , 'http://www.thunderboltgames.com/s/img600/grimfandango.jpg'
  , 'http://i2.listal.com/image/1425291/936full-grim-fandango-artwork.jpg'
  , 'http://www.xblafans.com/wp-content/uploads//2011/08/Grim-Fandango1.jpg'
  , 'http://media.giantbomb.com/uploads/0/1371/190604-grimfandango106_super.jpg'
  , 'http://gamejunkienz.files.wordpress.com/2012/02/grimfandango.jpg'
  , 'http://onlyhdwallpapers.com/wallpaper/video_games_grim_fandango_lucas_arts_desktop_1024x768_wallpaper-305343.jpg'
  , 'http://lparchive.org/Grim-Fandango-(Screenshot)/Update%207/02176.gif'
  , 'http://bulk2.destructoid.com/ul/128679-GrimFandangoActionFigures.jpg'
  , 'http://www.gamasutra.com/features/20061103/grimfandango02.jpg'
  , 'http://metavideogame.files.wordpress.com/2011/05/grimhof_03_1081459316.jpg'
  , 'http://3.bp.blogspot.com/_zBnIHQUy4r4/SpxdDw1Z8tI/AAAAAAAABJM/FoCWfc8imnc/s400/GrimFandango1024x768.jpg'
  , 'http://www.deviantart.com/download/184571597/grim_fandango_by_domigorgon-d31w0ct.jpg'
  , 'http://vgboxart.com/boxes/PC/29535-grim-fandango.png?t=1243105773'
  , 'http://kastatic.com/i2/games/1/3/13230/10.png'
  , 'http://www.thunderboltgames.com/s/img600/grimfandango.jpg'
  , 'http://i2.listal.com/image/1425291/936full-grim-fandango-artwork.jpg'
  , 'http://www.xblafans.com/wp-content/uploads//2011/08/Grim-Fandango1.jpg'
  , 'http://media.giantbomb.com/uploads/0/1371/190604-grimfandango106_super.jpg'
  , 'http://gamejunkienz.files.wordpress.com/2012/02/grimfandango.jpg'
  , 'http://onlyhdwallpapers.com/wallpaper/video_games_grim_fandango_lucas_arts_desktop_1024x768_wallpaper-305343.jpg'
  , 'http://lparchive.org/Grim-Fandango-(Screenshot)/Update%207/02176.gif'
  , 'http://bulk2.destructoid.com/ul/128679-GrimFandangoActionFigures.jpg'
  , 'http://www.gamasutra.com/features/20061103/grimfandango02.jpg'
  , 'http://metavideogame.files.wordpress.com/2011/05/grimhof_03_1081459316.jpg'
  , 'http://3.bp.blogspot.com/_zBnIHQUy4r4/SpxdDw1Z8tI/AAAAAAAABJM/FoCWfc8imnc/s400/GrimFandango1024x768.jpg'
  , 'http://www.deviantart.com/download/184571597/grim_fandango_by_domigorgon-d31w0ct.jpg'
  , 'http://vgboxart.com/boxes/PC/29535-grim-fandango.png?t=1243105773'
  , 'http://kastatic.com/i2/games/1/3/13230/10.png'
  , 'http://www.thunderboltgames.com/s/img600/grimfandango.jpg'
  , 'http://i2.listal.com/image/1425291/936full-grim-fandango-artwork.jpg'
];


================================================
FILE: examples/album/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Album</title>
    <script src="/page.js"></script>
    <link rel="stylesheet" href="/album/style.css" />
  </head>
  <body>
    <h1>Album</h1>
    <p></p>
    <ul id="photos"></ul>
    <div id="pager">
      <a id="prev" href="/">prev</a>
      <a id="next" href="/">next</a>
    </div>
    <p>View more <a href="https://www.google.com/search?q=grim+fandango">Grim Fandango</a> photos.</p>
    <script src="/album/app.js"></script>
  </body>
</html>


================================================
FILE: examples/album/style.css
================================================
body {
  padding: 50px;
  font: 200 16px "Helvetica Neue", Helvetica, Arial;
}

h1 {
  font-weight: 300;
}

a {
  color: #036dff;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

img {
  margin: 5px;
  width: 150px;
  height: 100px;
}

================================================
FILE: examples/basic/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Basic</title>
    <script src="/page.js"></script>
    <base href="/basic/" >
  </head>
  <body>
    <h1>Basic</h1>
    <p></p>
    <ul>
      <li><a href="./">/</a></li>
      <li><a href="#whoop">#whoop</a></li>
      <li><a href="./about">/about</a></li>
      <li><a href="./contact">/contact</a></li>
      <li><a href="./contact/me">/contact/me</a></li>
      <li><a href="./not-found?foo=bar">/not-found</a></li>
    </ul>

    <script>
      // the "notfound" implements a catch-all
      // with page('*', notfound). Here we have
      // no catch-all, so page.js will redirect
      // to the location of paths which do not
      // match any of the following routes
      //
      page.base('/basic');

      page('/', index);
      page('/about', about);
      page('/contact', contact);
      page('/contact/:contactName', contact);
      page();

      function index() {
        document.querySelector('p')
          .textContent = 'viewing index';
      }

      function about() {
        document.querySelector('p')
          .textContent = 'viewing about';
      }

      function contact(ctx) {
        document.querySelector('p')
          .textContent = 'viewing contact ' + (ctx.params.contactName || '');
      }
    </script>
  </body>
</html>


================================================
FILE: examples/chrome/chrome.css
================================================

* {
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}

aside {
  float: left;
  width: 120px;
}

body {
  margin: 0;
  font: 16px Helvetica, "Helvetica Neue", Arial, sans-serif;
  color: #5C6166;
}

h1,h2,h3 {
  margin: 0;
  font-weight: normal;
}

h1 {
  font-size: 18px;
  font-weight: 100;
  margin: 20px;
}

nav ul {
  margin: 0;
  padding: 0;
}

nav ul li {
  list-style: none;
  margin: 5px 0;
  padding: 0;
  padding-left: 15px;
  border-color: white;
  -webkit-transition: border-left 400ms;
  -moz-transition: border-left 400ms;
}

nav ul li.active {
  border-left: 5px solid #5C6166;
  padding-left: 10px;
}

nav ul li.active a {
  color: #5C6166;
}

nav ul li a {
  font-size: 13px;
  text-decoration: none;
  color: #999;
  font-weight: 100;
}

#content {
  padding: 20px;
  width: 500px;
  float: left;
  font-size: 14px;
  font-weight: 100;
  opacity: 1;
  margin-left: 0;
  -webkit-transition: opacity 200ms, margin-left 300ms;
  -moz-transition: opacity 200ms, margin-left 300ms;
}

#content.hide {
  opacity: 0;
  margin-left: -40px;
}

h2 {
  font-weight: normal;
  letter-spacing: 1px;
  font-size: 18px;
  border-bottom: 1px solid #eee;
}

================================================
FILE: examples/chrome/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Basic</title>
    <link rel="stylesheet" href="/chrome/chrome.css" type="text/css">
    <script src="/page.js"></script>
  </head>
  <body>
    <aside>
      <h1>Chrome</h1>
      <nav>
        <ul>
          <li><a href="./">History</a></li>
          <li><a href="./extensions">Extensions</a></li>
          <li><a href="./settings">Settings</a></li>
        </ul>
      </nav>
    </aside>

    <section id="content"></section>

    <script id="settings-template" type="text/x-template">
      <h2>Settings</h2>
      <p>The default browser is currently Google Chrome.</p>
    </script>

    <script id="extensions-template" type="text/x-template">
      <h2>Extensions</h2>
      <p>You currently have no browser extensions installed.</p>
    </script>

    <script id="history-template" type="text/x-template">
      <h2>History</h2>
      <p>Your browsing history will display here.</p>
    </script>

    <script id="not-found-template" type="text/x-template">
      <h2>Not Found</h2>
      <p>Sorry! I cannot find that page.</p>
    </script>

    <script>
      page.base('/chrome');
      page('*', showActiveLink);
      page('/', showHistory);
      page('/extensions', showExtensions);
      page('/settings', showSettings);
      page('*', notfound);
      page();

      function showActiveLink(ctx, next) {
        deactiveate();
        a(ctx.path).parentNode.classList.add('active');
        next();
      }

      function showHistory(ctx) {
        // !ctx.init tells render() not to
        // add the .hide class so that the
        // transition animation is ignored
        // for the initial page
        render(template('history'), !ctx.init);
      }
      
      function showExtensions(ctx) {
        render(template('extensions'), !ctx.init);
      }

      function showSettings(ctx) {
        render(template('settings'), !ctx.init);
      }

      function notfound(ctx) {
        render(template('not-found'), !ctx.init);
      }

      function render(html, hide) {
        var el = document.getElementById('content');
        if (hide) {
          el.classList.add('hide');
          setTimeout(function(){
            el.innerHTML = html;
            el.classList.remove('hide');
          }, 300);
        } else {
          el.innerHTML = html;
        }
      }

      function deactiveate() {
        var el = document.querySelector('.active')
        if (el) el.classList.remove('active');
      }

      function a(href) {
        return document.querySelector('[href=".' + href + '"]');
      }

      function template(name) {
        return document
          .getElementById(name + '-template')
          .innerHTML;
      }
    </script>
  </body>
</html>


================================================
FILE: examples/enterprisejs/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>EnterpriseJS</title>
    <script src="/page.js"></script>
  </head>
  <body>
    <h1>EnterpriseJS</h1>
    <p></p>
    <ul>
      <li><a href="./">/</a></li>
      <li><a href="./about">/about</a></li>
      <li><a href="./contact">/contact</a></li>
    </ul>

    <script>
      // if you're scared by non-"classes":

      function Router(obj) {
        var self = this;
        page.base('/enterprisejs');

        // define routes
        for (var key in obj) {
          if ('string' == typeof obj[key]) {
            ;(function(route, fn){
              page(key, function(){
                fn.apply(self, arguments);
              });
            })(key, obj[obj[key]]);
          }
        }

        this.render = function(text){
          document.querySelector('p').textContent = text;
        };

        this.start = page;
      }

      var router = new Router({
        '/': 'index',
        '/about': 'about',
        '/contact': 'contact',
        index: function(){ this.render('viewing index') },
        about: function(){ this.render('viewing about') },
        contact: function(){ this.render('viewing contact') }
      }).start();

    </script>
  </body>
</html>

================================================
FILE: examples/hash/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Hash</title>
    <style>
      body p {
        color: #333;
        font-family: verdana;
      }
      #sections p {
        height: 500px;
        margin: 30px;
        padding: 30px;
        line-height: 40px;
        background-color: #eee;
        border: 1px solid #ccb;
        font-size: 30px;
      }

      #sections p a {
        display: block;
        float: right;
        font-size: 14px;
      }

    </style>
    <script src="/page.js"></script>
  </head>
  <body>
    <h1 id="top">Hash</h1>

    <ul>
      <li><a href="#">#</a></li>
      <li><a href="#subsection">#subsection</a></li>
      <li><a href="section?name=tana">section?name=tana</a></li>
      <li><a href="section?name=tana#subsection">section?name=tana#subsection</a></li>
    </ul>

    <div id="sections">
      <p><strong>A</strong><a href="#top">top</a></p>
      <p><strong>B</strong><a href="#top">top</a></p>
      <p id="subsection"><strong>C</strong><a href="#top">top</a></p>
    </div>

    <script>
      page.base('/hash');
      page('/:section', section);
      page();

      function section(ctx, next) {
        console.log('path: ', ctx.path);
        console.log('querystring: ', ctx.querystring);
        console.log('hash: ', ctx.hash);
        console.log(' ');
      }
    </script>
  </body>
</html>


================================================
FILE: examples/hashbang/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Hashbang</title>
    <script src="/page.js"></script>
    <base href="/hashbang/">
  </head>
  <body>
    <h1>Hashbang</h1>
    <p></p>
    <ul>
      <li><a href="./">/</a></li>
      <li><a href="#whoop">#whoop</a></li>
      <li><a href="./about">/about</a></li>
      <li><a href="./contact">/contact</a></li>
      <li><a href="./contact/me">/contact/me</a></li>
      <li><a href="./not-found?foo=bar">/not-found</a></li>
    </ul>

    <script>
      page.base('/hashbang');
      page('/', index);
      page('/about', about);
      page('/contact', contact);
      page('/contact/:contactName', contact);
      page({
        hashbang:true
      });

      function index() {
        document.querySelector('p')
          .textContent = 'viewing index';
      }

      function about() {
        document.querySelector('p')
          .textContent = 'viewing about';
      }

      function contact(ctx) {
        document.querySelector('p')
          .textContent = 'viewing contact ' + (ctx.params.contactName || '');
      }
    </script>
  </body>
</html>


================================================
FILE: examples/index.js
================================================

/**
 * Module dependencies.
 */

var express = require('express')
  , join = require('path').join
  , fs = require('fs');

var app = express();


// deprecated express methods
// app.use(express.favicon());
// app.use(express.logger('dev'));

app.set('views', __dirname);
app.set('view engine', 'jade');
app.enable('strict routing');

// load examples

var examples = fs.readdirSync(__dirname).filter(function(file){
  return fs.statSync(__dirname + '/' + file).isDirectory();
});

// routes

/**
 * GET page.js
 */

app.get('/page.js', function(req, res){
  res.sendFile(join(__dirname, '..', 'page.js'));
});

/**
 * GET test libraries.
 */

app.get(/^\/(mocha|chai)\.(css|js)$/i, function(req, res){
  res.sendFile(join(__dirname, '../test/', req.params.join('.')));
});

/**
 * GET list of examples.
 */

app.get('/', function(req, res){
  res.render('list', { examples: examples });
});

/**
 * GET /:example -> /:example/
 */

app.get('/:example', function(req, res){
  res.redirect('/' + req.params.example + '/');
});

/**
 * GET /:example/* as a file if it exists.
 */

app.get('/:example/:file(*)', function(req, res, next){
  var file = req.params.file;
  if (!file) return next();
  var name = req.params.example;
  var path = join(__dirname, name, file);
  fs.stat(path, function(err, stat){
    if (err) return next();
    res.sendFile(path);
  });
});

/**
 * GET /:example/* as index.html
 */

app.get('/:example/*', function(req, res){
  var name = req.params.example;
  res.sendFile(join(__dirname, name, 'index.html'));
});

app.listen(4000);
console.log('Example server listening on port 4000');


================================================
FILE: examples/list.jade
================================================
doctype html
html
  head
    title page.js examples
  body
    h1 Examples
    ul
      for name in examples
        li
          a(href='/#{name}')= name
          if ('hash' == name)
            span
              |  -
              a(href="/hash/section?query=string") section
            span
              |  -
              a(href="/hash/section?query=string#subsection") subsection


================================================
FILE: examples/notfound/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Not Found</title>
    <script src="/page.js"></script>
  </head>
  <body>
    <h1>Not Found</h1>
    <p></p>
    <ul>
      <li><a href="./">/</a></li>
      <li><a href="./about">/about</a></li>
      <li><a href="./contact">/contact</a></li>
      <li><a href="./not-found">/not-found</a></li>
    </ul>

    <script>
      page.base('/notfound');
      page('/', index);
      page('/about', about);
      page('/contact', contact);
      page('*', notfound);
      page();

      function index() {
        document.querySelector('p')
          .textContent = 'viewing index';
      }
      
      function about() {
        document.querySelector('p')
          .textContent = 'viewing about';
      }

      function contact() {
        document.querySelector('p')
          .textContent = 'viewing contact';
      }

      function notfound() {
        document.querySelector('p')
          .textContent = 'not found';
      }
    </script>
  </body>
</html>

================================================
FILE: examples/partials/app.js
================================================

page.base(location.pathname.replace('/partials/', ''));
page('*', init.ctx);
page('/partials/home', route.home);
page('/partials/portfolio', route.portfolio);
page('/partials/about', route.about);
page('/partials/test', route.test);
page('/partials/examples', route.examples);
page('*', render.content);
page();


================================================
FILE: examples/partials/index.html
================================================
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
  <title>page.js partials</title>
  <link href="public/css/style.css" rel="stylesheet" type="text/css" media="all" />

  <script src="http://cdnjs.cloudflare.com/ajax/libs/hogan.js/2.0.0/hogan.js" type="text/javascript" charset="utf-8"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript" charset="utf-8"></script>

  <script src="/page.js" type="text/javascript" charset="utf-8"></script>

  <script src="public/js/events.js" type="text/javascript" charset="utf-8"></script>
  <script src="routes/index.js" type="text/javascript" charset="utf-8"></script>
  <script src="app.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<div class="shell">

  <nav id="navigation">
    <ul>
      <li><a href="home">home</a></li>
      <li><a href="portfolio">portfolio</a></li>
      <li><a href="about">about</a></li>
    </ul>
    <ul>
      <li><a href="test">show tests</a></li>
      <li><a href="examples">back to examples index</a></li>
    </ul>
  </nav>
  <section id="content">
    
  </section>

</div>
</body>
</html>


================================================
FILE: examples/partials/public/css/style.css
================================================
* { margin: 0; padding: 0; outline: 0; }

body { font-family: Arial, Helvetica, Sans-Serif; }


.shell { width: 800px; margin: 0 auto; }

#navigation { height: 100%; overflow: hidden; }
#navigation ul { list-style: none; display: inline-block; padding: 20px 0; }
#navigation li { display: inline; font-size: 16px; line-height: 19px; padding-right: 10px; }
#navigation li.active a { color: #c06; }
#navigation a { display: inline-block; color: #39f; text-decoration: none; }
#navigation ul:last-child { float: right; }
#navigation ul:first-child { float: none; }


================================================
FILE: examples/partials/public/js/events.js
================================================

$(function () {
  var navLinks = $('#navigation li');

  $('#navigation a').on('click', function (e) {	
    changeActive(navLinks.index(this));
  });

  window.changeActive = function (index) {
    navLinks.removeClass('active').eq(index).addClass('active');
  }
});


================================================
FILE: examples/partials/routes/index.js
================================================

(function () {
  // private api

  var cache = {};

  function get (url, cb) {
    if (cache[url]) return cb(cache[url]);
    $.ajax({
      url: url,
      success: function(data) {
        cache[url] = data;
        cb(data);
      },
      error: function(jqXHR, textStatus, errorThrown) {
        console.log(jqXHR, textStatus, errorThrown);
      },
      dataType: 'text'
    });
  }

  // public api

  window.init = {
    ctx: function (ctx, next) {
      ctx.data = {};
      ctx.partials = {};
      next();
    }
  };

  window.route = {
    home: function (ctx, next) {
      get('views/home.html', function (html) {
        ctx.data.index = 0;
        ctx.partials.content = html;
        next();
      });
    },
    portfolio: function (ctx, next) {
      get('views/portfolio.html', function (html) {
        ctx.data.index = 1;
        ctx.partials.content = html;
        next();
      });
    },
    about: function (ctx, next) {
      get('views/about.html', function (html) {
        ctx.data.index = 2;
        ctx.partials.content = html;
        next();
      });
    },
    test: function (ctx, next) {
      window.location.href = 'http://localhost:4000/partials/test/';
    },
    examples: function (ctx, next) {
      window.location.href = 'http://localhost:4000/';
    }
  };

  window.render = {
    content: function (ctx, next) {
      get('views/content.html', function (html) {
        var template = Hogan.compile(html),
          content = template.render(ctx.data, ctx.partials);
        //
        $('#content').empty().append(content);
        changeActive(ctx.data.index);
        if (typeof done === 'function') done(ctx.data.index);
      });
    }
  };

  window.done = null;
}());


================================================
FILE: examples/partials/test/index.html
================================================
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
  <title>page.js partials tests</title>
  <link href="../public/css/style.css" rel="stylesheet" type="text/css" media="all" />
  <link href="/mocha.css" rel="stylesheet" type="text/css" media="all" />
  <!-- test libs -->
  <script src="/mocha.js"></script>
  <script src="/chai.js"></script>
  <!-- test files -->
  <script src="tests.js"></script>
</head>
<body>
  <nav id="navigation">
    <ul>
      <li><a href="/partials">back to partials example</a></li>
      <li><a href="/">back to examples index</a></li>
    </ul>
  </nav>
  <iframe src="../index.html" id="site"></iframe>
  <div id="mocha"></div>
</body>
</html>


================================================
FILE: examples/partials/test/tests.js
================================================

var expect = chai.expect;
mocha.setup({
  ui: 'bdd',
  globals: ['']
});

var site = null;
window.addEventListener('load', function (e) {
  site = document.getElementById('site');
  mocha.run();
});

describe('site', function () {
  function expected (index, idx, header) {
    expect(index).to.equal(idx);
    var h = site.contentDocument.querySelector('#content h1');
    expect(h.innerHTML).to.equal(header);
    var p = site.contentDocument.querySelector('#content p');
    expect(p.innerHTML).to.equal('page '+index);
  }
  
  it('should load home page', function (done) {
    site.contentWindow.done = function (index) {
      expected(index, 0, 'Home');
      done();
    }
    site.contentWindow.page('/partials/home');
  });
  it('should load portfolio page', function (done) {
    site.contentWindow.done = function (index) {
      expected(index, 1, 'Portfolio');
      done();
    }
    site.contentWindow.page('/partials/portfolio');
  });
  it('should load about page', function (done) {
    site.contentWindow.done = function (index) {
      expected(index, 2, 'About');
      done();
    }
    site.contentWindow.page('/partials/about');
  });

  after(function (done) {
    site.contentWindow.done = null;
    done();
  });
});


================================================
FILE: examples/partials/views/about.html
================================================
<h1>About</h1>
<p>page {{index}}</p>

================================================
FILE: examples/partials/views/content.html
================================================
{{>content}}

================================================
FILE: examples/partials/views/home.html
================================================
<h1>Home</h1>
<p>page {{index}}</p>

================================================
FILE: examples/partials/views/portfolio.html
================================================
<h1>Portfolio</h1>
<p>page {{index}}</p>

================================================
FILE: examples/profile/app.js
================================================

var avatars = {
  glottis: 'http://homepage.ntlworld.com/stureek/images/glottis03.jpg',
  manny: 'http://kprojekt.net/wp-content/uploads/manny.jpg',
  sal: 'http://homepage.ntlworld.com/stureek/images/sal01.jpg'
};

page.base('/profile');
page('/', index);
// display the index page again after 5s
// only for user related pages
page('/user/*', displayIndexAfter(5000));
// you dont need to use two,
// but this demonstrates how
// you can "filter" requests
// then move on to the next callback
// to separate concerns
page('/user/:name', load);
page('/user/:name', show);
// or:
// page('/user/:name', load, show);
page('*', notfound);
page();

// everything below is not part of page.js
// just callbacks etc..

document.querySelector('#cycle').onclick = function(e){
  var i = 0;
  var names = Object.keys(avatars);
  setInterval(function(){
    var name = names[i++ % names.length];
    page('/user/' + name);
  }, 1500);
};

function text(str) {
  document.querySelector('p').textContent = str;
}

function displayIndexAfter(ms) {
  var id;
  return function(ctx, next){
    id && clearTimeout(id);

    if ('/' != ctx.path) {
      id = setTimeout(function(){
        page('/');
      }, ms);
    }
    next();
  }
}

function index() {
  text('Click a user below to load their avatar');
  document.querySelector('img')
    .style.display = 'none';
}

function load(ctx, next) {
  ctx.avatar = avatars[ctx.params.name];
  next();
}

function show(ctx) {
  var img = document.querySelector('img');
  img.src = ctx.avatar;
  img.style.display = 'block';
  text('Showing ' + ctx.params.name);
}

function notfound() {
  document.querySelector('p')
    .textContent = 'not found';
}

================================================
FILE: examples/profile/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Profile</title>
    <script src="/page.js"></script>
    <link rel="stylesheet" href="/profile/style.css" />
  </head>
  <body>
    <h1>Profile</h1>
    <p></p>
    <img width=150 />

    <ul>
      <li><a href="/profile/user/manny">Manny</a></li>
      <li><a href="/profile/user/glottis">Glottis</a></li>
      <li><a href="/profile/user/sal">Sal</a></li>
    </ul>

    <a href="#" id="cycle">Cycle through users</a>
    <script src="/profile/app.js"></script>
  </body>
</html>


================================================
FILE: examples/profile/style.css
================================================
body {
  padding: 50px;
  font: 200 16px "Helvetica Neue", Helvetica, Arial;
}

h1 {
  font-weight: 300;
}

a {
  color: #036dff;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

================================================
FILE: examples/query-string/app.js
================================================

page.base('/query-string');
page('*', parse)
page('/', show)
page()

function parse(ctx, next) {
  ctx.query = qs.parse(location.search.slice(1));
  next();
}

function show(ctx) {
  if (Object.keys(ctx.query).length) {
    document
      .querySelector('pre')
      .textContent = JSON.stringify(ctx.query, null, 2);
  }
}

================================================
FILE: examples/query-string/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Query string</title>
    <link rel="stylesheet" href="/query-string/style.css" />
  </head>
  <body>
    <h1>Query string</h1>
    <pre>Add a query-string! Maybe: user[name]=tobi</pre>
    <script src="/page.js"></script>
    <script src="/query-string/query.js"></script>
    <script src="/query-string/app.js"></script>
  </body>
</html>


================================================
FILE: examples/query-string/query.js
================================================
(function(exports){

/**
 * Library version.
 */

exports.version = '0.4.2';

/**
 * Object#toString() ref for stringify().
 */

var toString = Object.prototype.toString;

/**
 * Cache non-integer test regexp.
 */

var isint = /^[0-9]+$/;

function promote(parent, key) {
  if (parent[key].length == 0) return parent[key] = {};
  var t = {};
  for (var i in parent[key]) t[i] = parent[key][i];
  parent[key] = t;
  return t;
}

function parse(parts, parent, key, val) {
  var part = parts.shift();
  // end
  if (!part) {
    if (Array.isArray(parent[key])) {
      parent[key].push(val);
    } else if ('object' == typeof parent[key]) {
      parent[key] = val;
    } else if ('undefined' == typeof parent[key]) {
      parent[key] = val;
    } else {
      parent[key] = [parent[key], val];
    }
    // array
  } else {
    var obj = parent[key] = parent[key] || [];
    if (']' == part) {
      if (Array.isArray(obj)) {
        if ('' != val) obj.push(val);
      } else if ('object' == typeof obj) {
        obj[Object.keys(obj).length] = val;
      } else {
        obj = parent[key] = [parent[key], val];
      }
      // prop
    } else if (~part.indexOf(']')) {
      part = part.substr(0, part.length - 1);
      if (!isint.test(part) && Array.isArray(obj)) obj = promote(parent, key);
      parse(parts, obj, part, val);
      // key
    } else {
      if (!isint.test(part) && Array.isArray(obj)) obj = promote(parent, key);
      parse(parts, obj, part, val);
    }
  }
}

/**
 * Merge parent key/val pair.
 */

function merge(parent, key, val){
  if (~key.indexOf(']')) {
    var parts = key.split('[')
      , len = parts.length
      , last = len - 1;
    parse(parts, parent, 'base', val);
    // optimize
  } else {
    if (!isint.test(key) && Array.isArray(parent.base)) {
      var t = {};
      for (var k in parent.base) t[k] = parent.base[k];
      parent.base = t;
    }
    set(parent.base, key, val);
  }

  return parent;
}

/**
 * Parse the given obj.
 */

function parseObject(obj){
  var ret = { base: {} };
  Object.keys(obj).forEach(function(name){
    merge(ret, name, obj[name]);
  });
  return ret.base;
}

/**
 * Parse the given str.
 */

function parseString(str){
  return String(str)
    .split('&')
    .reduce(function(ret, pair){
      try{
        pair = decodeURIComponent(pair.replace(/\+/g, ' '));
      } catch(e) {
        // ignore
      }

      var eql = pair.indexOf('=')
        , brace = lastBraceInKey(pair)
        , key = pair.substr(0, brace || eql)
        , val = pair.substr(brace || eql, pair.length)
        , val = val.substr(val.indexOf('=') + 1, val.length);

      // ?foo
      if ('' == key) key = pair, val = '';

      return merge(ret, key, val);
    }, { base: {} }).base;
}

/**
 * Parse the given query `str` or `obj`, returning an object.
 *
 * @param {String} str | {Object} obj
 * @return {Object}
 * @api public
 */

exports.parse = function(str){
  if (null == str || '' == str) return {};
  return 'object' == typeof str
    ? parseObject(str)
    : parseString(str);
};

/**
 * Turn the given `obj` into a query string
 *
 * @param {Object} obj
 * @return {String}
 * @api public
 */

var stringify = exports.stringify = function(obj, prefix) {
  if (Array.isArray(obj)) {
    return stringifyArray(obj, prefix);
  } else if ('[object Object]' == toString.call(obj)) {
    return stringifyObject(obj, prefix);
  } else if ('string' == typeof obj) {
    return stringifyString(obj, prefix);
  } else {
    return prefix + '=' + obj;
  }
};

/**
 * Stringify the given `str`.
 *
 * @param {String} str
 * @param {String} prefix
 * @return {String}
 * @api private
 */

function stringifyString(str, prefix) {
  if (!prefix) throw new TypeError('stringify expects an object');
  return prefix + '=' + encodeURIComponent(str);
}

/**
 * Stringify the given `arr`.
 *
 * @param {Array} arr
 * @param {String} prefix
 * @return {String}
 * @api private
 */

function stringifyArray(arr, prefix) {
  var ret = [];
  if (!prefix) throw new TypeError('stringify expects an object');
  for (var i = 0; i < arr.length; i++) {
    ret.push(stringify(arr[i], prefix + '['+i+']'));
  }
  return ret.join('&');
}

/**
 * Stringify the given `obj`.
 *
 * @param {Object} obj
 * @param {String} prefix
 * @return {String}
 * @api private
 */

function stringifyObject(obj, prefix) {
  var ret = []
    , keys = Object.keys(obj)
    , key;

  for (var i = 0, len = keys.length; i < len; ++i) {
    key = keys[i];
    ret.push(stringify(obj[key], prefix
      ? prefix + '[' + encodeURIComponent(key) + ']'
      : encodeURIComponent(key)));
  }

  return ret.join('&');
}

/**
 * Set `obj`'s `key` to `val` respecting
 * the weird and wonderful syntax of a qs,
 * where "foo=bar&foo=baz" becomes an array.
 *
 * @param {Object} obj
 * @param {String} key
 * @param {String} val
 * @api private
 */

function set(obj, key, val) {
  var v = obj[key];
  if (undefined === v) {
    obj[key] = val;
  } else if (Array.isArray(v)) {
    v.push(val);
  } else {
    obj[key] = [v, val];
  }
}

/**
 * Locate last brace in `str` within the key.
 *
 * @param {String} str
 * @return {Number}
 * @api private
 */

function lastBraceInKey(str) {
  var len = str.length
    , brace
    , c;
  for (var i = 0; i < len; ++i) {
    c = str[i];
    if (']' == c) brace = false;
    if ('[' == c) brace = true;
    if ('=' == c && !brace) return i;
  }
}
})('undefined' == typeof exports ? qs = {} : exports);

================================================
FILE: examples/query-string/style.css
================================================
body {
  padding: 50px;
  font: 200 16px "Helvetica Neue", Helvetica, Arial;
}

h1 {
  font-weight: 300;
}

a {
  color: #036dff;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

================================================
FILE: examples/server/app.js
================================================

page.base('/server');
page('/', index)
page('/contact', contact)
page({ dispatch: false })

function index() {
  document.querySelector('p')
    .textContent = 'Index page generated on the client!';
}

function contact() {
  document.querySelector('p')
    .textContent = 'Contact page generated on the client!';
}

================================================
FILE: examples/server/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Server</title>
    <script src="/page.js"></script>
  </head>
  <body>
    <h1>Server</h1>
    <p>Initial content generated by the server!</p>
    <ul>
      <li><a href="./">Home</a></li>
      <li><a href="./contact">Contact</a></li>
    </ul>

    <script src="/server/app.js"></script>
  </body>
</html>


================================================
FILE: examples/server/style.css
================================================
body {
  padding: 50px;
  font: 200 16px "Helvetica Neue", Helvetica, Arial;
}

h1 {
  font-weight: 300;
}

a {
  color: #036dff;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}


================================================
FILE: examples/state/app.js
================================================

var avatars = {
  glottis: 'http://homepage.ntlworld.com/stureek/images/glottis03.jpg',
  manny: 'http://kprojekt.net/wp-content/uploads/manny.jpg',
  sal: 'http://homepage.ntlworld.com/stureek/images/sal01.jpg'
};

page.base('/state');
page('/', index);
page('/user/:name', load, show);
page('*', notfound);
page();

// everything below is not part of page.js
// just callbacks etc..

function text(str) {
  document.querySelector('p').textContent = str;
}

function index() {
  text('Click a user below to load their avatar');
  document.querySelector('img')
    .style.display = 'none';
}

function load(ctx, next) {
  // check if we have .state.avatar already available
  // this could for example be a cached html fragment.
  if (ctx.state.avatar) {
    ctx.avatar = ctx.state.avatar;
    next();
    return;
  }

  // pretend we're querying some database etc
  setTimeout(function(){
    // you can assign properties to the context
    // for use between these functions. The .state
    // property is what's saved in history.
    ctx.state.avatar = ctx.avatar = avatars[ctx.params.name];
    ctx.save();
    next();
  }, 600);
}

function show(ctx) {
  var img = document.querySelector('img');
  img.src = ctx.avatar;
  img.style.display = 'block';
  text('Showing ' + ctx.params.name);
}

function notfound() {
  document.querySelector('p')
    .textContent = 'not found';
}

================================================
FILE: examples/state/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Profile</title>
    <script src="/page.js"></script>
    <link rel="stylesheet" href="/state/style.css" />
  </head>
  <body>
    <h1>Profile</h1>
    <p></p>
    <img width=150 />

    <ul>
      <li><a href="/state/user/manny">Manny</a></li>
      <li><a href="/state/user/glottis">Glottis</a></li>
      <li><a href="/state/user/sal">Sal</a></li>
    </ul>

    <script src="/state/app.js"></script>
  </body>
</html>


================================================
FILE: examples/state/style.css
================================================
body {
  padding: 50px;
  font: 200 16px "Helvetica Neue", Helvetica, Arial;
}

h1 {
  font-weight: 300;
}

a {
  color: #036dff;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}


================================================
FILE: examples/transitions/app.js
================================================

// content

var content = document.querySelector('#content');

// current page indicator

var p = document.querySelector('#page');

// "mount" it

page.base('/transitions');

// transition "middleware"

page('*', function(ctx,  next){
  if (ctx.init) {
    next();
  } else {
    content.classList.add('transition');
    setTimeout(function(){
      content.classList.remove('transition');
      next();
    }, 300);
  }
})

// regular pages

page('/', function(){
  p.textContent = '';
});

page('/contact', function(){
  p.textContent = 'contact page';
});

page('/about', function(){
  p.textContent = 'about page';
});

page()

================================================
FILE: examples/transitions/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Page.js - transitions</title>
    <script src="/page.js"></script>
    <link rel="stylesheet" href="/transitions/style.css" />
  </head>
  <body>
    <section id="content">
      <img src="/transitions/logo.png" id="logo" />

      <p id="page"></p>

      <nav>
        <a href="/transitions">home</a>
        <a href="/transitions/contact">contact</a>
        <a href="/transitions/about">about</a>
      </nav>
    </section>
    <script src="/transitions/app.js"></script>
  </body>
</html>


================================================
FILE: examples/transitions/style.css
================================================

body {
  background: url(/transitions/bg.png);
  text-align: center;
  font: 200 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
  letter-spacing: .1em;
}

#content {
  margin: 100px auto;
  padding: 100px 50px 80px 50px;
  width: 500px;
  background: white;
  position: relative;
  opacity: 1;
  -webkit-border-radius: 2px;
  -webkit-box-shadow: 0 0 10px black;
  -webkit-transition: -webkit-transform 300ms ease-out, opacity 300ms ease-out;
}

#content.transition {
  -webkit-transform: translateX(100px);
  opacity: 0;
}

#page {
  margin-top: 40px;
}

nav {
  position: absolute;
  bottom: -26px;
  left: 0;
}

nav a {
  text-decoration: none;
  color: rgba(255,255,255,.3);
  background: -webkit-linear-gradient(#444, #3a3a3a);
  display: inline-block;
  padding: 5px 10px;
  font-size: 12px;
  -webkit-box-shadow: 0 1px 3px rgba(0,0,0,.6), inset 0 -1px 0 rgba(255,255,255,.05);
  -webkit-border-radius: 0 0 2px 2px;
}

nav a:hover {
  color: white;
}

nav a:active {
  -webkit-box-shadow: 0 0 2px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
}

================================================
FILE: index.js
================================================
  /* globals require, module */

  'use strict';

  /**
   * Module dependencies.
   */

  var pathtoRegexp = require('path-to-regexp');

  /**
   * Short-cuts for global-object checks
   */

  var hasDocument = ('undefined' !== typeof document);
  var hasWindow = ('undefined' !== typeof window);
  var hasHistory = ('undefined' !== typeof history);
  var hasProcess = typeof process !== 'undefined';

  /**
   * Detect click event
   */
  var clickEvent = hasDocument && document.ontouchstart ? 'touchstart' : 'click';

  /**
   * To work properly with the URL
   * history.location generated polyfill in https://github.com/devote/HTML5-History-API
   */

  var isLocation = hasWindow && !!(window.history.location || window.location);

  /**
   * The page instance
   * @api private
   */
  function Page() {
    // public things
    this.callbacks = [];
    this.exits = [];
    this.current = '';
    this.len = 0;

    // private things
    this._decodeURLComponents = true;
    this._base = '';
    this._strict = false;
    this._running = false;
    this._hashbang = false;

    // bound functions
    this.clickHandler = this.clickHandler.bind(this);
    this._onpopstate = this._onpopstate.bind(this);
  }

  /**
   * Configure the instance of page. This can be called multiple times.
   *
   * @param {Object} options
   * @api public
   */

  Page.prototype.configure = function(options) {
    var opts = options || {};

    this._window = opts.window || (hasWindow && window);
    this._decodeURLComponents = opts.decodeURLComponents !== false;
    this._popstate = opts.popstate !== false && hasWindow;
    this._click = opts.click !== false && hasDocument;
    this._hashbang = !!opts.hashbang;

    var _window = this._window;
    if(this._popstate) {
      _window.addEventListener('popstate', this._onpopstate, false);
    } else if(hasWindow) {
      _window.removeEventListener('popstate', this._onpopstate, false);
    }

    if (this._click) {
      _window.document.addEventListener(clickEvent, this.clickHandler, false);
    } else if(hasDocument) {
      _window.document.removeEventListener(clickEvent, this.clickHandler, false);
    }

    if(this._hashbang && hasWindow && !hasHistory) {
      _window.addEventListener('hashchange', this._onpopstate, false);
    } else if(hasWindow) {
      _window.removeEventListener('hashchange', this._onpopstate, false);
    }
  };

  /**
   * Get or set basepath to `path`.
   *
   * @param {string} path
   * @api public
   */

  Page.prototype.base = function(path) {
    if (0 === arguments.length) return this._base;
    this._base = path;
  };

  /**
   * Gets the `base`, which depends on whether we are using History or
   * hashbang routing.

   * @api private
   */
  Page.prototype._getBase = function() {
    var base = this._base;
    if(!!base) return base;
    var loc = hasWindow && this._window && this._window.location;

    if(hasWindow && this._hashbang && loc && loc.protocol === 'file:') {
      base = loc.pathname;
    }

    return base;
  };

  /**
   * Get or set strict path matching to `enable`
   *
   * @param {boolean} enable
   * @api public
   */

  Page.prototype.strict = function(enable) {
    if (0 === arguments.length) return this._strict;
    this._strict = enable;
  };


  /**
   * Bind with the given `options`.
   *
   * Options:
   *
   *    - `click` bind to click events [true]
   *    - `popstate` bind to popstate [true]
   *    - `dispatch` perform initial dispatch [true]
   *
   * @param {Object} options
   * @api public
   */

  Page.prototype.start = function(options) {
    var opts = options || {};
    this.configure(opts);

    if (false === opts.dispatch) return;
    this._running = true;

    var url;
    if(isLocation) {
      var window = this._window;
      var loc = window.location;

      if(this._hashbang && ~loc.hash.indexOf('#!')) {
        url = loc.hash.substr(2) + loc.search;
      } else if (this._hashbang) {
        url = loc.search + loc.hash;
      } else {
        url = loc.pathname + loc.search + loc.hash;
      }
    }

    this.replace(url, null, true, opts.dispatch);
  };

  /**
   * Unbind click and popstate event handlers.
   *
   * @api public
   */

  Page.prototype.stop = function() {
    if (!this._running) return;
    this.current = '';
    this.len = 0;
    this._running = false;

    var window = this._window;
    this._click && window.document.removeEventListener(clickEvent, this.clickHandler, false);
    hasWindow && window.removeEventListener('popstate', this._onpopstate, false);
    hasWindow && window.removeEventListener('hashchange', this._onpopstate, false);
  };

  /**
   * Show `path` with optional `state` object.
   *
   * @param {string} path
   * @param {Object=} state
   * @param {boolean=} dispatch
   * @param {boolean=} push
   * @return {!Context}
   * @api public
   */

  Page.prototype.show = function(path, state, dispatch, push) {
    var ctx = new Context(path, state, this),
      prev = this.prevContext;
    this.prevContext = ctx;
    this.current = ctx.path;
    if (false !== dispatch) this.dispatch(ctx, prev);
    if (false !== ctx.handled && false !== push) ctx.pushState();
    return ctx;
  };

  /**
   * Goes back in the history
   * Back should always let the current route push state and then go back.
   *
   * @param {string} path - fallback path to go back if no more history exists, if undefined defaults to page.base
   * @param {Object=} state
   * @api public
   */

  Page.prototype.back = function(path, state) {
    var page = this;
    if (this.len > 0) {
      var window = this._window;
      // this may need more testing to see if all browsers
      // wait for the next tick to go back in history
      hasHistory && window.history.back();
      this.len--;
    } else if (path) {
      setTimeout(function() {
        page.show(path, state);
      });
    } else {
      setTimeout(function() {
        page.show(page._getBase(), state);
      });
    }
  };

  /**
   * Register route to redirect from one path to other
   * or just redirect to another route
   *
   * @param {string} from - if param 'to' is undefined redirects to 'from'
   * @param {string=} to
   * @api public
   */
  Page.prototype.redirect = function(from, to) {
    var inst = this;

    // Define route from a path to another
    if ('string' === typeof from && 'string' === typeof to) {
      page.call(this, from, function(e) {
        setTimeout(function() {
          inst.replace(/** @type {!string} */ (to));
        }, 0);
      });
    }

    // Wait for the push state and replace it with another
    if ('string' === typeof from && 'undefined' === typeof to) {
      setTimeout(function() {
        inst.replace(from);
      }, 0);
    }
  };

  /**
   * Replace `path` with optional `state` object.
   *
   * @param {string} path
   * @param {Object=} state
   * @param {boolean=} init
   * @param {boolean=} dispatch
   * @return {!Context}
   * @api public
   */


  Page.prototype.replace = function(path, state, init, dispatch) {
    var ctx = new Context(path, state, this),
      prev = this.prevContext;
    this.prevContext = ctx;
    this.current = ctx.path;
    ctx.init = init;
    ctx.save(); // save before dispatching, which may redirect
    if (false !== dispatch) this.dispatch(ctx, prev);
    return ctx;
  };

  /**
   * Dispatch the given `ctx`.
   *
   * @param {Context} ctx
   * @api private
   */

  Page.prototype.dispatch = function(ctx, prev) {
    var i = 0, j = 0, page = this;

    function nextExit() {
      var fn = page.exits[j++];
      if (!fn) return nextEnter();
      fn(prev, nextExit);
    }

    function nextEnter() {
      var fn = page.callbacks[i++];

      if (ctx.path !== page.current) {
        ctx.handled = false;
        return;
      }
      if (!fn) return unhandled.call(page, ctx);
      fn(ctx, nextEnter);
    }

    if (prev) {
      nextExit();
    } else {
      nextEnter();
    }
  };

  /**
   * Register an exit route on `path` with
   * callback `fn()`, which will be called
   * on the previous context when a new
   * page is visited.
   */
  Page.prototype.exit = function(path, fn) {
    if (typeof path === 'function') {
      return this.exit('*', path);
    }

    var route = new Route(path, null, this);
    for (var i = 1; i < arguments.length; ++i) {
      this.exits.push(route.middleware(arguments[i]));
    }
  };

  /**
   * Handle "click" events.
   */

  /* jshint +W054 */
  Page.prototype.clickHandler = function(e) {
    if (1 !== this._which(e)) return;

    if (e.metaKey || e.ctrlKey || e.shiftKey) return;
    if (e.defaultPrevented) return;

    // ensure link
    // use shadow dom when available if not, fall back to composedPath()
    // for browsers that only have shady
    var el = e.target;
    var eventPath = e.path || (e.composedPath ? e.composedPath() : null);

    if(eventPath) {
      for (var i = 0; i < eventPath.length; i++) {
        if (!eventPath[i].nodeName) continue;
        if (eventPath[i].nodeName.toUpperCase() !== 'A') continue;
        if (!eventPath[i].href) continue;

        el = eventPath[i];
        break;
      }
    }

    // continue ensure link
    // el.nodeName for svg links are 'a' instead of 'A'
    while (el && 'A' !== el.nodeName.toUpperCase()) el = el.parentNode;
    if (!el || 'A' !== el.nodeName.toUpperCase()) return;

    // check if link is inside an svg
    // in this case, both href and target are always inside an object
    var svg = (typeof el.href === 'object') && el.href.constructor.name === 'SVGAnimatedString';

    // Ignore if tag has
    // 1. "download" attribute
    // 2. rel="external" attribute
    if (el.hasAttribute('download') || el.getAttribute('rel') === 'external') return;

    // ensure non-hash for the same path
    var link = el.getAttribute('href');
    if(!this._hashbang && this._samePath(el) && (el.hash || '#' === link)) return;

    // Check for mailto: in the href
    if (link && link.indexOf('mailto:') > -1) return;

    // check target
    // svg target is an object and its desired value is in .baseVal property
    if (svg ? el.target.baseVal : el.target) return;

    // x-origin
    // note: svg links that are not relative don't call click events (and skip page.js)
    // consequently, all svg links tested inside page.js are relative and in the same origin
    if (!svg && !this.sameOrigin(el.href)) return;

    // rebuild path
    // There aren't .pathname and .search properties in svg links, so we use href
    // Also, svg href is an object and its desired value is in .baseVal property
    var path = svg ? el.href.baseVal : (el.pathname + el.search + (el.hash || ''));

    path = path[0] !== '/' ? '/' + path : path;

    // strip leading "/[drive letter]:" on NW.js on Windows
    if (hasProcess && path.match(/^\/[a-zA-Z]:\//)) {
      path = path.replace(/^\/[a-zA-Z]:\//, '/');
    }

    // same page
    var orig = path;
    var pageBase = this._getBase();

    if (path.indexOf(pageBase) === 0) {
      path = path.substr(pageBase.length);
    }

    if (this._hashbang) path = path.replace('#!', '');

    if (pageBase && orig === path && (!isLocation || this._window.location.protocol !== 'file:')) {
      return;
    }

    e.preventDefault();
    this.show(orig);
  };

  /**
   * Handle "populate" events.
   * @api private
   */

  Page.prototype._onpopstate = (function () {
    var loaded = false;
    if ( ! hasWindow ) {
      return function () {};
    }
    if (hasDocument && document.readyState === 'complete') {
      loaded = true;
    } else {
      window.addEventListener('load', function() {
        setTimeout(function() {
          loaded = true;
        }, 0);
      });
    }
    return function onpopstate(e) {
      if (!loaded) return;
      var page = this;
      if (e.state) {
        var path = e.state.path;
        page.replace(path, e.state);
      } else if (isLocation) {
        var loc = page._window.location;
        page.show(loc.pathname + loc.search + loc.hash, undefined, undefined, false);
      }
    };
  })();

  /**
   * Event button.
   */
  Page.prototype._which = function(e) {
    e = e || (hasWindow && this._window.event);
    return null == e.which ? e.button : e.which;
  };

  /**
   * Convert to a URL object
   * @api private
   */
  Page.prototype._toURL = function(href) {
    var window = this._window;
    if(typeof URL === 'function' && isLocation) {
      return new URL(href, window.location.toString());
    } else if (hasDocument) {
      var anc = window.document.createElement('a');
      anc.href = href;
      return anc;
    }
  };

  /**
   * Check if `href` is the same origin.
   * @param {string} href
   * @api public
   */
  Page.prototype.sameOrigin = function(href) {
    if(!href || !isLocation) return false;

    var url = this._toURL(href);
    var window = this._window;

    var loc = window.location;

    /*
       When the port is the default http port 80 for http, or 443 for
       https, internet explorer 11 returns an empty string for loc.port,
       so we need to compare loc.port with an empty string if url.port
       is the default port 80 or 443.
       Also the comparition with `port` is changed from `===` to `==` because
       `port` can be a string sometimes. This only applies to ie11.
    */
    return loc.protocol === url.protocol &&
      loc.hostname === url.hostname &&
      (loc.port === url.port || loc.port === '' && (url.port == 80 || url.port == 443)); // jshint ignore:line
  };

  /**
   * @api private
   */
  Page.prototype._samePath = function(url) {
    if(!isLocation) return false;
    var window = this._window;
    var loc = window.location;
    return url.pathname === loc.pathname &&
      url.search === loc.search;
  };

  /**
   * Remove URL encoding from the given `str`.
   * Accommodates whitespace in both x-www-form-urlencoded
   * and regular percent-encoded form.
   *
   * @param {string} val - URL component to decode
   * @api private
   */
  Page.prototype._decodeURLEncodedURIComponent = function(val) {
    if (typeof val !== 'string') { return val; }
    return this._decodeURLComponents ? decodeURIComponent(val.replace(/\+/g, ' ')) : val;
  };

  /**
   * Create a new `page` instance and function
   */
  function createPage() {
    var pageInstance = new Page();

    function pageFn(/* args */) {
      return page.apply(pageInstance, arguments);
    }

    // Copy all of the things over. In 2.0 maybe we use setPrototypeOf
    pageFn.callbacks = pageInstance.callbacks;
    pageFn.exits = pageInstance.exits;
    pageFn.base = pageInstance.base.bind(pageInstance);
    pageFn.strict = pageInstance.strict.bind(pageInstance);
    pageFn.start = pageInstance.start.bind(pageInstance);
    pageFn.stop = pageInstance.stop.bind(pageInstance);
    pageFn.show = pageInstance.show.bind(pageInstance);
    pageFn.back = pageInstance.back.bind(pageInstance);
    pageFn.redirect = pageInstance.redirect.bind(pageInstance);
    pageFn.replace = pageInstance.replace.bind(pageInstance);
    pageFn.dispatch = pageInstance.dispatch.bind(pageInstance);
    pageFn.exit = pageInstance.exit.bind(pageInstance);
    pageFn.configure = pageInstance.configure.bind(pageInstance);
    pageFn.sameOrigin = pageInstance.sameOrigin.bind(pageInstance);
    pageFn.clickHandler = pageInstance.clickHandler.bind(pageInstance);

    pageFn.create = createPage;

    Object.defineProperty(pageFn, 'len', {
      get: function(){
        return pageInstance.len;
      },
      set: function(val) {
        pageInstance.len = val;
      }
    });

    Object.defineProperty(pageFn, 'current', {
      get: function(){
        return pageInstance.current;
      },
      set: function(val) {
        pageInstance.current = val;
      }
    });

    // In 2.0 these can be named exports
    pageFn.Context = Context;
    pageFn.Route = Route;

    return pageFn;
  }

  /**
   * Register `path` with callback `fn()`,
   * or route `path`, or redirection,
   * or `page.start()`.
   *
   *   page(fn);
   *   page('*', fn);
   *   page('/user/:id', load, user);
   *   page('/user/' + user.id, { some: 'thing' });
   *   page('/user/' + user.id);
   *   page('/from', '/to')
   *   page();
   *
   * @param {string|!Function|!Object} path
   * @param {Function=} fn
   * @api public
   */

  function page(path, fn) {
    // <callback>
    if ('function' === typeof path) {
      return page.call(this, '*', path);
    }

    // route <path> to <callback ...>
    if ('function' === typeof fn) {
      var route = new Route(/** @type {string} */ (path), null, this);
      for (var i = 1; i < arguments.length; ++i) {
        this.callbacks.push(route.middleware(arguments[i]));
      }
      // show <path> with [state]
    } else if ('string' === typeof path) {
      this['string' === typeof fn ? 'redirect' : 'show'](path, fn);
      // start [options]
    } else {
      this.start(path);
    }
  }

  /**
   * Unhandled `ctx`. When it's not the initial
   * popstate then redirect. If you wish to handle
   * 404s on your own use `page('*', callback)`.
   *
   * @param {Context} ctx
   * @api private
   */
  function unhandled(ctx) {
    if (ctx.handled) return;
    var current;
    var page = this;
    var window = page._window;

    if (page._hashbang) {
      current = isLocation && this._getBase() + window.location.hash.replace('#!', '');
    } else {
      current = isLocation && window.location.pathname + window.location.search;
    }

    if (current === ctx.canonicalPath) return;
    page.stop();
    ctx.handled = false;
    isLocation && (window.location.href = ctx.canonicalPath);
  }

  /**
   * Escapes RegExp characters in the given string.
   *
   * @param {string} s
   * @api private
   */
  function escapeRegExp(s) {
    return s.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
  }

  /**
   * Initialize a new "request" `Context`
   * with the given `path` and optional initial `state`.
   *
   * @constructor
   * @param {string} path
   * @param {Object=} state
   * @api public
   */

  function Context(path, state, pageInstance) {
    var _page = this.page = pageInstance || page;
    var window = _page._window;
    var hashbang = _page._hashbang;

    var pageBase = _page._getBase();
    if ('/' === path[0] && 0 !== path.indexOf(pageBase)) path = pageBase + (hashbang ? '#!' : '') + path;
    var i = path.indexOf('?');

    this.canonicalPath = path;
    var re = new RegExp('^' + escapeRegExp(pageBase));
    this.path = path.replace(re, '') || '/';
    if (hashbang) this.path = this.path.replace('#!', '') || '/';

    this.title = (hasDocument && window.document.title);
    this.state = state || {};
    this.state.path = path;
    this.querystring = ~i ? _page._decodeURLEncodedURIComponent(path.slice(i + 1)) : '';
    this.pathname = _page._decodeURLEncodedURIComponent(~i ? path.slice(0, i) : path);
    this.params = {};

    // fragment
    this.hash = '';
    if (!hashbang) {
      if (!~this.path.indexOf('#')) return;
      var parts = this.path.split('#');
      this.path = this.pathname = parts[0];
      this.hash = _page._decodeURLEncodedURIComponent(parts[1]) || '';
      this.querystring = this.querystring.split('#')[0];
    }
  }

  /**
   * Push state.
   *
   * @api private
   */

  Context.prototype.pushState = function() {
    var page = this.page;
    var window = page._window;
    var hashbang = page._hashbang;

    page.len++;
    if (hasHistory) {
        window.history.pushState(this.state, this.title,
          hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
    }
  };

  /**
   * Save the context state.
   *
   * @api public
   */

  Context.prototype.save = function() {
    var page = this.page;
    if (hasHistory) {
        page._window.history.replaceState(this.state, this.title,
          page._hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
    }
  };

  /**
   * Initialize `Route` with the given HTTP `path`,
   * and an array of `callbacks` and `options`.
   *
   * Options:
   *
   *   - `sensitive`    enable case-sensitive routes
   *   - `strict`       enable strict matching for trailing slashes
   *
   * @constructor
   * @param {string} path
   * @param {Object=} options
   * @api private
   */

  function Route(path, options, page) {
    var _page = this.page = page || globalPage;
    var opts = options || {};
    opts.strict = opts.strict || _page._strict;
    this.path = (path === '*') ? '(.*)' : path;
    this.method = 'GET';
    this.regexp = pathtoRegexp(this.path, this.keys = [], opts);
  }

  /**
   * Return route middleware with
   * the given callback `fn()`.
   *
   * @param {Function} fn
   * @return {Function}
   * @api public
   */

  Route.prototype.middleware = function(fn) {
    var self = this;
    return function(ctx, next) {
      if (self.match(ctx.path, ctx.params)) {
        ctx.routePath = self.path;
        return fn(ctx, next);
      }
      next();
    };
  };

  /**
   * Check if this route matches `path`, if so
   * populate `params`.
   *
   * @param {string} path
   * @param {Object} params
   * @return {boolean}
   * @api private
   */

  Route.prototype.match = function(path, params) {
    var keys = this.keys,
      qsIndex = path.indexOf('?'),
      pathname = ~qsIndex ? path.slice(0, qsIndex) : path,
      m = this.regexp.exec(decodeURIComponent(pathname));

    if (!m) return false;

    delete params[0];

    for (var i = 1, len = m.length; i < len; ++i) {
      var key = keys[i - 1];
      var val = this.page._decodeURLEncodedURIComponent(m[i]);
      if (val !== undefined || !(hasOwnProperty.call(params, key.name))) {
        params[key.name] = val;
      }
    }

    return true;
  };


  /**
   * Module exports.
   */

  var globalPage = createPage();
  module.exports = globalPage;
  module.exports.default = globalPage;


================================================
FILE: package.json
================================================
{
  "name": "page",
  "description": "Tiny client-side router",
  "version": "1.11.6",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git://github.com/visionmedia/page.js.git"
  },
  "component": {
    "scripts": {
      "page": "index.js"
    }
  },
  "main": "index.js",
  "browser": "page.js",
  "module": "page.mjs",
  "scripts": {
    "engine-deps": "install-engine-dependencies",
    "test": "jshint index.js test/tests.js && mocha test/tests.js",
    "serve": "serve test --symlinks",
    "test-cov": "jscoverage index.js index-cov.js; PAGE_COV=1 mocha test/tests.js -R html-cov > coverage.html",
    "make": "rollup -c rollup.config.js"
  },
  "dependencies": {
    "path-to-regexp": "~1.2.1"
  },
  "devDependencies": {
    "chai": "^1.10.0",
    "coveralls": "^2.11.2",
    "engine-dependencies": "^0.2.12",
    "express": "^4.10.2",
    "jade": "^1.7.0",
    "jscoverage": "^0.5.9",
    "jsdom": "^11.5.1",
    "jshint": "^2.5.10",
    "mocha": "^2.0.1",
    "mocha-lcov-reporter": "0.0.1",
    "rollup": "^0.54.1",
    "rollup-plugin-commonjs": "^8.2.6",
    "rollup-plugin-node-resolve": "^3.0.2",
    "serve": "*",
    "should": "*"
  },
  "engineDependencies": {
    "node": {
      "0.10.x": {
        "devDependencies": {
          "jsdom": "^1.3.1"
        }
      }
    }
  },
  "files": [
    "index.js",
    "page.js",
    "page.mjs"
  ]
}


================================================
FILE: page.js
================================================
(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
	typeof define === 'function' && define.amd ? define(factory) :
	(global.page = factory());
}(this, (function () { 'use strict';

var isarray = Array.isArray || function (arr) {
  return Object.prototype.toString.call(arr) == '[object Array]';
};

/**
 * Expose `pathToRegexp`.
 */
var pathToRegexp_1 = pathToRegexp;
var parse_1 = parse;
var compile_1 = compile;
var tokensToFunction_1 = tokensToFunction;
var tokensToRegExp_1 = tokensToRegExp;

/**
 * The main path matching regexp utility.
 *
 * @type {RegExp}
 */
var PATH_REGEXP = new RegExp([
  // Match escaped characters that would otherwise appear in future matches.
  // This allows the user to escape special characters that won't transform.
  '(\\\\.)',
  // Match Express-style parameters and un-named parameters with a prefix
  // and optional suffixes. Matches appear as:
  //
  // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
  // "/route(\\d+)"  => [undefined, undefined, undefined, "\d+", undefined, undefined]
  // "/*"            => ["/", undefined, undefined, undefined, undefined, "*"]
  '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^()])+)\\))?|\\(((?:\\\\.|[^()])+)\\))([+*?])?|(\\*))'
].join('|'), 'g');

/**
 * Parse a string for the raw tokens.
 *
 * @param  {String} str
 * @return {Array}
 */
function parse (str) {
  var tokens = [];
  var key = 0;
  var index = 0;
  var path = '';
  var res;

  while ((res = PATH_REGEXP.exec(str)) != null) {
    var m = res[0];
    var escaped = res[1];
    var offset = res.index;
    path += str.slice(index, offset);
    index = offset + m.length;

    // Ignore already escaped sequences.
    if (escaped) {
      path += escaped[1];
      continue
    }

    // Push the current path onto the tokens.
    if (path) {
      tokens.push(path);
      path = '';
    }

    var prefix = res[2];
    var name = res[3];
    var capture = res[4];
    var group = res[5];
    var suffix = res[6];
    var asterisk = res[7];

    var repeat = suffix === '+' || suffix === '*';
    var optional = suffix === '?' || suffix === '*';
    var delimiter = prefix || '/';
    var pattern = capture || group || (asterisk ? '.*' : '[^' + delimiter + ']+?');

    tokens.push({
      name: name || key++,
      prefix: prefix || '',
      delimiter: delimiter,
      optional: optional,
      repeat: repeat,
      pattern: escapeGroup(pattern)
    });
  }

  // Match any characters still remaining.
  if (index < str.length) {
    path += str.substr(index);
  }

  // If the path exists, push it onto the end.
  if (path) {
    tokens.push(path);
  }

  return tokens
}

/**
 * Compile a string to a template function for the path.
 *
 * @param  {String}   str
 * @return {Function}
 */
function compile (str) {
  return tokensToFunction(parse(str))
}

/**
 * Expose a method for transforming tokens into the path function.
 */
function tokensToFunction (tokens) {
  // Compile all the tokens into regexps.
  var matches = new Array(tokens.length);

  // Compile all the patterns before compilation.
  for (var i = 0; i < tokens.length; i++) {
    if (typeof tokens[i] === 'object') {
      matches[i] = new RegExp('^' + tokens[i].pattern + '$');
    }
  }

  return function (obj) {
    var path = '';
    var data = obj || {};

    for (var i = 0; i < tokens.length; i++) {
      var token = tokens[i];

      if (typeof token === 'string') {
        path += token;

        continue
      }

      var value = data[token.name];
      var segment;

      if (value == null) {
        if (token.optional) {
          continue
        } else {
          throw new TypeError('Expected "' + token.name + '" to be defined')
        }
      }

      if (isarray(value)) {
        if (!token.repeat) {
          throw new TypeError('Expected "' + token.name + '" to not repeat, but received "' + value + '"')
        }

        if (value.length === 0) {
          if (token.optional) {
            continue
          } else {
            throw new TypeError('Expected "' + token.name + '" to not be empty')
          }
        }

        for (var j = 0; j < value.length; j++) {
          segment = encodeURIComponent(value[j]);

          if (!matches[i].test(segment)) {
            throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
          }

          path += (j === 0 ? token.prefix : token.delimiter) + segment;
        }

        continue
      }

      segment = encodeURIComponent(value);

      if (!matches[i].test(segment)) {
        throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
      }

      path += token.prefix + segment;
    }

    return path
  }
}

/**
 * Escape a regular expression string.
 *
 * @param  {String} str
 * @return {String}
 */
function escapeString (str) {
  return str.replace(/([.+*?=^!:${}()[\]|\/])/g, '\\$1')
}

/**
 * Escape the capturing group by escaping special characters and meaning.
 *
 * @param  {String} group
 * @return {String}
 */
function escapeGroup (group) {
  return group.replace(/([=!:$\/()])/g, '\\$1')
}

/**
 * Attach the keys as a property of the regexp.
 *
 * @param  {RegExp} re
 * @param  {Array}  keys
 * @return {RegExp}
 */
function attachKeys (re, keys) {
  re.keys = keys;
  return re
}

/**
 * Get the flags for a regexp from the options.
 *
 * @param  {Object} options
 * @return {String}
 */
function flags (options) {
  return options.sensitive ? '' : 'i'
}

/**
 * Pull out keys from a regexp.
 *
 * @param  {RegExp} path
 * @param  {Array}  keys
 * @return {RegExp}
 */
function regexpToRegexp (path, keys) {
  // Use a negative lookahead to match only capturing groups.
  var groups = path.source.match(/\((?!\?)/g);

  if (groups) {
    for (var i = 0; i < groups.length; i++) {
      keys.push({
        name: i,
        prefix: null,
        delimiter: null,
        optional: false,
        repeat: false,
        pattern: null
      });
    }
  }

  return attachKeys(path, keys)
}

/**
 * Transform an array into a regexp.
 *
 * @param  {Array}  path
 * @param  {Array}  keys
 * @param  {Object} options
 * @return {RegExp}
 */
function arrayToRegexp (path, keys, options) {
  var parts = [];

  for (var i = 0; i < path.length; i++) {
    parts.push(pathToRegexp(path[i], keys, options).source);
  }

  var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options));

  return attachKeys(regexp, keys)
}

/**
 * Create a path regexp from string input.
 *
 * @param  {String} path
 * @param  {Array}  keys
 * @param  {Object} options
 * @return {RegExp}
 */
function stringToRegexp (path, keys, options) {
  var tokens = parse(path);
  var re = tokensToRegExp(tokens, options);

  // Attach keys back to the regexp.
  for (var i = 0; i < tokens.length; i++) {
    if (typeof tokens[i] !== 'string') {
      keys.push(tokens[i]);
    }
  }

  return attachKeys(re, keys)
}

/**
 * Expose a function for taking tokens and returning a RegExp.
 *
 * @param  {Array}  tokens
 * @param  {Array}  keys
 * @param  {Object} options
 * @return {RegExp}
 */
function tokensToRegExp (tokens, options) {
  options = options || {};

  var strict = options.strict;
  var end = options.end !== false;
  var route = '';
  var lastToken = tokens[tokens.length - 1];
  var endsWithSlash = typeof lastToken === 'string' && /\/$/.test(lastToken);

  // Iterate over the tokens and create our regexp string.
  for (var i = 0; i < tokens.length; i++) {
    var token = tokens[i];

    if (typeof token === 'string') {
      route += escapeString(token);
    } else {
      var prefix = escapeString(token.prefix);
      var capture = token.pattern;

      if (token.repeat) {
        capture += '(?:' + prefix + capture + ')*';
      }

      if (token.optional) {
        if (prefix) {
          capture = '(?:' + prefix + '(' + capture + '))?';
        } else {
          capture = '(' + capture + ')?';
        }
      } else {
        capture = prefix + '(' + capture + ')';
      }

      route += capture;
    }
  }

  // In non-strict mode we allow a slash at the end of match. If the path to
  // match already ends with a slash, we remove it for consistency. The slash
  // is valid at the end of a path match, not in the middle. This is important
  // in non-ending mode, where "/test/" shouldn't match "/test//route".
  if (!strict) {
    route = (endsWithSlash ? route.slice(0, -2) : route) + '(?:\\/(?=$))?';
  }

  if (end) {
    route += '$';
  } else {
    // In non-ending mode, we need the capturing groups to match as much as
    // possible by using a positive lookahead to the end or next path segment.
    route += strict && endsWithSlash ? '' : '(?=\\/|$)';
  }

  return new RegExp('^' + route, flags(options))
}

/**
 * Normalize the given path string, returning a regular expression.
 *
 * An empty array can be passed in for the keys, which will hold the
 * placeholder key descriptions. For example, using `/user/:id`, `keys` will
 * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
 *
 * @param  {(String|RegExp|Array)} path
 * @param  {Array}                 [keys]
 * @param  {Object}                [options]
 * @return {RegExp}
 */
function pathToRegexp (path, keys, options) {
  keys = keys || [];

  if (!isarray(keys)) {
    options = keys;
    keys = [];
  } else if (!options) {
    options = {};
  }

  if (path instanceof RegExp) {
    return regexpToRegexp(path, keys, options)
  }

  if (isarray(path)) {
    return arrayToRegexp(path, keys, options)
  }

  return stringToRegexp(path, keys, options)
}

pathToRegexp_1.parse = parse_1;
pathToRegexp_1.compile = compile_1;
pathToRegexp_1.tokensToFunction = tokensToFunction_1;
pathToRegexp_1.tokensToRegExp = tokensToRegExp_1;

/**
   * Module dependencies.
   */

  

  /**
   * Short-cuts for global-object checks
   */

  var hasDocument = ('undefined' !== typeof document);
  var hasWindow = ('undefined' !== typeof window);
  var hasHistory = ('undefined' !== typeof history);
  var hasProcess = typeof process !== 'undefined';

  /**
   * Detect click event
   */
  var clickEvent = hasDocument && document.ontouchstart ? 'touchstart' : 'click';

  /**
   * To work properly with the URL
   * history.location generated polyfill in https://github.com/devote/HTML5-History-API
   */

  var isLocation = hasWindow && !!(window.history.location || window.location);

  /**
   * The page instance
   * @api private
   */
  function Page() {
    // public things
    this.callbacks = [];
    this.exits = [];
    this.current = '';
    this.len = 0;

    // private things
    this._decodeURLComponents = true;
    this._base = '';
    this._strict = false;
    this._running = false;
    this._hashbang = false;

    // bound functions
    this.clickHandler = this.clickHandler.bind(this);
    this._onpopstate = this._onpopstate.bind(this);
  }

  /**
   * Configure the instance of page. This can be called multiple times.
   *
   * @param {Object} options
   * @api public
   */

  Page.prototype.configure = function(options) {
    var opts = options || {};

    this._window = opts.window || (hasWindow && window);
    this._decodeURLComponents = opts.decodeURLComponents !== false;
    this._popstate = opts.popstate !== false && hasWindow;
    this._click = opts.click !== false && hasDocument;
    this._hashbang = !!opts.hashbang;

    var _window = this._window;
    if(this._popstate) {
      _window.addEventListener('popstate', this._onpopstate, false);
    } else if(hasWindow) {
      _window.removeEventListener('popstate', this._onpopstate, false);
    }

    if (this._click) {
      _window.document.addEventListener(clickEvent, this.clickHandler, false);
    } else if(hasDocument) {
      _window.document.removeEventListener(clickEvent, this.clickHandler, false);
    }

    if(this._hashbang && hasWindow && !hasHistory) {
      _window.addEventListener('hashchange', this._onpopstate, false);
    } else if(hasWindow) {
      _window.removeEventListener('hashchange', this._onpopstate, false);
    }
  };

  /**
   * Get or set basepath to `path`.
   *
   * @param {string} path
   * @api public
   */

  Page.prototype.base = function(path) {
    if (0 === arguments.length) return this._base;
    this._base = path;
  };

  /**
   * Gets the `base`, which depends on whether we are using History or
   * hashbang routing.

   * @api private
   */
  Page.prototype._getBase = function() {
    var base = this._base;
    if(!!base) return base;
    var loc = hasWindow && this._window && this._window.location;

    if(hasWindow && this._hashbang && loc && loc.protocol === 'file:') {
      base = loc.pathname;
    }

    return base;
  };

  /**
   * Get or set strict path matching to `enable`
   *
   * @param {boolean} enable
   * @api public
   */

  Page.prototype.strict = function(enable) {
    if (0 === arguments.length) return this._strict;
    this._strict = enable;
  };


  /**
   * Bind with the given `options`.
   *
   * Options:
   *
   *    - `click` bind to click events [true]
   *    - `popstate` bind to popstate [true]
   *    - `dispatch` perform initial dispatch [true]
   *
   * @param {Object} options
   * @api public
   */

  Page.prototype.start = function(options) {
    var opts = options || {};
    this.configure(opts);

    if (false === opts.dispatch) return;
    this._running = true;

    var url;
    if(isLocation) {
      var window = this._window;
      var loc = window.location;

      if(this._hashbang && ~loc.hash.indexOf('#!')) {
        url = loc.hash.substr(2) + loc.search;
      } else if (this._hashbang) {
        url = loc.search + loc.hash;
      } else {
        url = loc.pathname + loc.search + loc.hash;
      }
    }

    this.replace(url, null, true, opts.dispatch);
  };

  /**
   * Unbind click and popstate event handlers.
   *
   * @api public
   */

  Page.prototype.stop = function() {
    if (!this._running) return;
    this.current = '';
    this.len = 0;
    this._running = false;

    var window = this._window;
    this._click && window.document.removeEventListener(clickEvent, this.clickHandler, false);
    hasWindow && window.removeEventListener('popstate', this._onpopstate, false);
    hasWindow && window.removeEventListener('hashchange', this._onpopstate, false);
  };

  /**
   * Show `path` with optional `state` object.
   *
   * @param {string} path
   * @param {Object=} state
   * @param {boolean=} dispatch
   * @param {boolean=} push
   * @return {!Context}
   * @api public
   */

  Page.prototype.show = function(path, state, dispatch, push) {
    var ctx = new Context(path, state, this),
      prev = this.prevContext;
    this.prevContext = ctx;
    this.current = ctx.path;
    if (false !== dispatch) this.dispatch(ctx, prev);
    if (false !== ctx.handled && false !== push) ctx.pushState();
    return ctx;
  };

  /**
   * Goes back in the history
   * Back should always let the current route push state and then go back.
   *
   * @param {string} path - fallback path to go back if no more history exists, if undefined defaults to page.base
   * @param {Object=} state
   * @api public
   */

  Page.prototype.back = function(path, state) {
    var page = this;
    if (this.len > 0) {
      var window = this._window;
      // this may need more testing to see if all browsers
      // wait for the next tick to go back in history
      hasHistory && window.history.back();
      this.len--;
    } else if (path) {
      setTimeout(function() {
        page.show(path, state);
      });
    } else {
      setTimeout(function() {
        page.show(page._getBase(), state);
      });
    }
  };

  /**
   * Register route to redirect from one path to other
   * or just redirect to another route
   *
   * @param {string} from - if param 'to' is undefined redirects to 'from'
   * @param {string=} to
   * @api public
   */
  Page.prototype.redirect = function(from, to) {
    var inst = this;

    // Define route from a path to another
    if ('string' === typeof from && 'string' === typeof to) {
      page.call(this, from, function(e) {
        setTimeout(function() {
          inst.replace(/** @type {!string} */ (to));
        }, 0);
      });
    }

    // Wait for the push state and replace it with another
    if ('string' === typeof from && 'undefined' === typeof to) {
      setTimeout(function() {
        inst.replace(from);
      }, 0);
    }
  };

  /**
   * Replace `path` with optional `state` object.
   *
   * @param {string} path
   * @param {Object=} state
   * @param {boolean=} init
   * @param {boolean=} dispatch
   * @return {!Context}
   * @api public
   */


  Page.prototype.replace = function(path, state, init, dispatch) {
    var ctx = new Context(path, state, this),
      prev = this.prevContext;
    this.prevContext = ctx;
    this.current = ctx.path;
    ctx.init = init;
    ctx.save(); // save before dispatching, which may redirect
    if (false !== dispatch) this.dispatch(ctx, prev);
    return ctx;
  };

  /**
   * Dispatch the given `ctx`.
   *
   * @param {Context} ctx
   * @api private
   */

  Page.prototype.dispatch = function(ctx, prev) {
    var i = 0, j = 0, page = this;

    function nextExit() {
      var fn = page.exits[j++];
      if (!fn) return nextEnter();
      fn(prev, nextExit);
    }

    function nextEnter() {
      var fn = page.callbacks[i++];

      if (ctx.path !== page.current) {
        ctx.handled = false;
        return;
      }
      if (!fn) return unhandled.call(page, ctx);
      fn(ctx, nextEnter);
    }

    if (prev) {
      nextExit();
    } else {
      nextEnter();
    }
  };

  /**
   * Register an exit route on `path` with
   * callback `fn()`, which will be called
   * on the previous context when a new
   * page is visited.
   */
  Page.prototype.exit = function(path, fn) {
    if (typeof path === 'function') {
      return this.exit('*', path);
    }

    var route = new Route(path, null, this);
    for (var i = 1; i < arguments.length; ++i) {
      this.exits.push(route.middleware(arguments[i]));
    }
  };

  /**
   * Handle "click" events.
   */

  /* jshint +W054 */
  Page.prototype.clickHandler = function(e) {
    if (1 !== this._which(e)) return;

    if (e.metaKey || e.ctrlKey || e.shiftKey) return;
    if (e.defaultPrevented) return;

    // ensure link
    // use shadow dom when available if not, fall back to composedPath()
    // for browsers that only have shady
    var el = e.target;
    var eventPath = e.path || (e.composedPath ? e.composedPath() : null);

    if(eventPath) {
      for (var i = 0; i < eventPath.length; i++) {
        if (!eventPath[i].nodeName) continue;
        if (eventPath[i].nodeName.toUpperCase() !== 'A') continue;
        if (!eventPath[i].href) continue;

        el = eventPath[i];
        break;
      }
    }

    // continue ensure link
    // el.nodeName for svg links are 'a' instead of 'A'
    while (el && 'A' !== el.nodeName.toUpperCase()) el = el.parentNode;
    if (!el || 'A' !== el.nodeName.toUpperCase()) return;

    // check if link is inside an svg
    // in this case, both href and target are always inside an object
    var svg = (typeof el.href === 'object') && el.href.constructor.name === 'SVGAnimatedString';

    // Ignore if tag has
    // 1. "download" attribute
    // 2. rel="external" attribute
    if (el.hasAttribute('download') || el.getAttribute('rel') === 'external') return;

    // ensure non-hash for the same path
    var link = el.getAttribute('href');
    if(!this._hashbang && this._samePath(el) && (el.hash || '#' === link)) return;

    // Check for mailto: in the href
    if (link && link.indexOf('mailto:') > -1) return;

    // check target
    // svg target is an object and its desired value is in .baseVal property
    if (svg ? el.target.baseVal : el.target) return;

    // x-origin
    // note: svg links that are not relative don't call click events (and skip page.js)
    // consequently, all svg links tested inside page.js are relative and in the same origin
    if (!svg && !this.sameOrigin(el.href)) return;

    // rebuild path
    // There aren't .pathname and .search properties in svg links, so we use href
    // Also, svg href is an object and its desired value is in .baseVal property
    var path = svg ? el.href.baseVal : (el.pathname + el.search + (el.hash || ''));

    path = path[0] !== '/' ? '/' + path : path;

    // strip leading "/[drive letter]:" on NW.js on Windows
    if (hasProcess && path.match(/^\/[a-zA-Z]:\//)) {
      path = path.replace(/^\/[a-zA-Z]:\//, '/');
    }

    // same page
    var orig = path;
    var pageBase = this._getBase();

    if (path.indexOf(pageBase) === 0) {
      path = path.substr(pageBase.length);
    }

    if (this._hashbang) path = path.replace('#!', '');

    if (pageBase && orig === path && (!isLocation || this._window.location.protocol !== 'file:')) {
      return;
    }

    e.preventDefault();
    this.show(orig);
  };

  /**
   * Handle "populate" events.
   * @api private
   */

  Page.prototype._onpopstate = (function () {
    var loaded = false;
    if ( ! hasWindow ) {
      return function () {};
    }
    if (hasDocument && document.readyState === 'complete') {
      loaded = true;
    } else {
      window.addEventListener('load', function() {
        setTimeout(function() {
          loaded = true;
        }, 0);
      });
    }
    return function onpopstate(e) {
      if (!loaded) return;
      var page = this;
      if (e.state) {
        var path = e.state.path;
        page.replace(path, e.state);
      } else if (isLocation) {
        var loc = page._window.location;
        page.show(loc.pathname + loc.search + loc.hash, undefined, undefined, false);
      }
    };
  })();

  /**
   * Event button.
   */
  Page.prototype._which = function(e) {
    e = e || (hasWindow && this._window.event);
    return null == e.which ? e.button : e.which;
  };

  /**
   * Convert to a URL object
   * @api private
   */
  Page.prototype._toURL = function(href) {
    var window = this._window;
    if(typeof URL === 'function' && isLocation) {
      return new URL(href, window.location.toString());
    } else if (hasDocument) {
      var anc = window.document.createElement('a');
      anc.href = href;
      return anc;
    }
  };

  /**
   * Check if `href` is the same origin.
   * @param {string} href
   * @api public
   */
  Page.prototype.sameOrigin = function(href) {
    if(!href || !isLocation) return false;

    var url = this._toURL(href);
    var window = this._window;

    var loc = window.location;

    /*
       When the port is the default http port 80 for http, or 443 for
       https, internet explorer 11 returns an empty string for loc.port,
       so we need to compare loc.port with an empty string if url.port
       is the default port 80 or 443.
       Also the comparition with `port` is changed from `===` to `==` because
       `port` can be a string sometimes. This only applies to ie11.
    */
    return loc.protocol === url.protocol &&
      loc.hostname === url.hostname &&
      (loc.port === url.port || loc.port === '' && (url.port == 80 || url.port == 443)); // jshint ignore:line
  };

  /**
   * @api private
   */
  Page.prototype._samePath = function(url) {
    if(!isLocation) return false;
    var window = this._window;
    var loc = window.location;
    return url.pathname === loc.pathname &&
      url.search === loc.search;
  };

  /**
   * Remove URL encoding from the given `str`.
   * Accommodates whitespace in both x-www-form-urlencoded
   * and regular percent-encoded form.
   *
   * @param {string} val - URL component to decode
   * @api private
   */
  Page.prototype._decodeURLEncodedURIComponent = function(val) {
    if (typeof val !== 'string') { return val; }
    return this._decodeURLComponents ? decodeURIComponent(val.replace(/\+/g, ' ')) : val;
  };

  /**
   * Create a new `page` instance and function
   */
  function createPage() {
    var pageInstance = new Page();

    function pageFn(/* args */) {
      return page.apply(pageInstance, arguments);
    }

    // Copy all of the things over. In 2.0 maybe we use setPrototypeOf
    pageFn.callbacks = pageInstance.callbacks;
    pageFn.exits = pageInstance.exits;
    pageFn.base = pageInstance.base.bind(pageInstance);
    pageFn.strict = pageInstance.strict.bind(pageInstance);
    pageFn.start = pageInstance.start.bind(pageInstance);
    pageFn.stop = pageInstance.stop.bind(pageInstance);
    pageFn.show = pageInstance.show.bind(pageInstance);
    pageFn.back = pageInstance.back.bind(pageInstance);
    pageFn.redirect = pageInstance.redirect.bind(pageInstance);
    pageFn.replace = pageInstance.replace.bind(pageInstance);
    pageFn.dispatch = pageInstance.dispatch.bind(pageInstance);
    pageFn.exit = pageInstance.exit.bind(pageInstance);
    pageFn.configure = pageInstance.configure.bind(pageInstance);
    pageFn.sameOrigin = pageInstance.sameOrigin.bind(pageInstance);
    pageFn.clickHandler = pageInstance.clickHandler.bind(pageInstance);

    pageFn.create = createPage;

    Object.defineProperty(pageFn, 'len', {
      get: function(){
        return pageInstance.len;
      },
      set: function(val) {
        pageInstance.len = val;
      }
    });

    Object.defineProperty(pageFn, 'current', {
      get: function(){
        return pageInstance.current;
      },
      set: function(val) {
        pageInstance.current = val;
      }
    });

    // In 2.0 these can be named exports
    pageFn.Context = Context;
    pageFn.Route = Route;

    return pageFn;
  }

  /**
   * Register `path` with callback `fn()`,
   * or route `path`, or redirection,
   * or `page.start()`.
   *
   *   page(fn);
   *   page('*', fn);
   *   page('/user/:id', load, user);
   *   page('/user/' + user.id, { some: 'thing' });
   *   page('/user/' + user.id);
   *   page('/from', '/to')
   *   page();
   *
   * @param {string|!Function|!Object} path
   * @param {Function=} fn
   * @api public
   */

  function page(path, fn) {
    // <callback>
    if ('function' === typeof path) {
      return page.call(this, '*', path);
    }

    // route <path> to <callback ...>
    if ('function' === typeof fn) {
      var route = new Route(/** @type {string} */ (path), null, this);
      for (var i = 1; i < arguments.length; ++i) {
        this.callbacks.push(route.middleware(arguments[i]));
      }
      // show <path> with [state]
    } else if ('string' === typeof path) {
      this['string' === typeof fn ? 'redirect' : 'show'](path, fn);
      // start [options]
    } else {
      this.start(path);
    }
  }

  /**
   * Unhandled `ctx`. When it's not the initial
   * popstate then redirect. If you wish to handle
   * 404s on your own use `page('*', callback)`.
   *
   * @param {Context} ctx
   * @api private
   */
  function unhandled(ctx) {
    if (ctx.handled) return;
    var current;
    var page = this;
    var window = page._window;

    if (page._hashbang) {
      current = isLocation && this._getBase() + window.location.hash.replace('#!', '');
    } else {
      current = isLocation && window.location.pathname + window.location.search;
    }

    if (current === ctx.canonicalPath) return;
    page.stop();
    ctx.handled = false;
    isLocation && (window.location.href = ctx.canonicalPath);
  }

  /**
   * Escapes RegExp characters in the given string.
   *
   * @param {string} s
   * @api private
   */
  function escapeRegExp(s) {
    return s.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
  }

  /**
   * Initialize a new "request" `Context`
   * with the given `path` and optional initial `state`.
   *
   * @constructor
   * @param {string} path
   * @param {Object=} state
   * @api public
   */

  function Context(path, state, pageInstance) {
    var _page = this.page = pageInstance || page;
    var window = _page._window;
    var hashbang = _page._hashbang;

    var pageBase = _page._getBase();
    if ('/' === path[0] && 0 !== path.indexOf(pageBase)) path = pageBase + (hashbang ? '#!' : '') + path;
    var i = path.indexOf('?');

    this.canonicalPath = path;
    var re = new RegExp('^' + escapeRegExp(pageBase));
    this.path = path.replace(re, '') || '/';
    if (hashbang) this.path = this.path.replace('#!', '') || '/';

    this.title = (hasDocument && window.document.title);
    this.state = state || {};
    this.state.path = path;
    this.querystring = ~i ? _page._decodeURLEncodedURIComponent(path.slice(i + 1)) : '';
    this.pathname = _page._decodeURLEncodedURIComponent(~i ? path.slice(0, i) : path);
    this.params = {};

    // fragment
    this.hash = '';
    if (!hashbang) {
      if (!~this.path.indexOf('#')) return;
      var parts = this.path.split('#');
      this.path = this.pathname = parts[0];
      this.hash = _page._decodeURLEncodedURIComponent(parts[1]) || '';
      this.querystring = this.querystring.split('#')[0];
    }
  }

  /**
   * Push state.
   *
   * @api private
   */

  Context.prototype.pushState = function() {
    var page = this.page;
    var window = page._window;
    var hashbang = page._hashbang;

    page.len++;
    if (hasHistory) {
        window.history.pushState(this.state, this.title,
          hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
    }
  };

  /**
   * Save the context state.
   *
   * @api public
   */

  Context.prototype.save = function() {
    var page = this.page;
    if (hasHistory) {
        page._window.history.replaceState(this.state, this.title,
          page._hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
    }
  };

  /**
   * Initialize `Route` with the given HTTP `path`,
   * and an array of `callbacks` and `options`.
   *
   * Options:
   *
   *   - `sensitive`    enable case-sensitive routes
   *   - `strict`       enable strict matching for trailing slashes
   *
   * @constructor
   * @param {string} path
   * @param {Object=} options
   * @api private
   */

  function Route(path, options, page) {
    var _page = this.page = page || globalPage;
    var opts = options || {};
    opts.strict = opts.strict || _page._strict;
    this.path = (path === '*') ? '(.*)' : path;
    this.method = 'GET';
    this.regexp = pathToRegexp_1(this.path, this.keys = [], opts);
  }

  /**
   * Return route middleware with
   * the given callback `fn()`.
   *
   * @param {Function} fn
   * @return {Function}
   * @api public
   */

  Route.prototype.middleware = function(fn) {
    var self = this;
    return function(ctx, next) {
      if (self.match(ctx.path, ctx.params)) {
        ctx.routePath = self.path;
        return fn(ctx, next);
      }
      next();
    };
  };

  /**
   * Check if this route matches `path`, if so
   * populate `params`.
   *
   * @param {string} path
   * @param {Object} params
   * @return {boolean}
   * @api private
   */

  Route.prototype.match = function(path, params) {
    var keys = this.keys,
      qsIndex = path.indexOf('?'),
      pathname = ~qsIndex ? path.slice(0, qsIndex) : path,
      m = this.regexp.exec(decodeURIComponent(pathname));

    if (!m) return false;

    delete params[0];

    for (var i = 1, len = m.length; i < len; ++i) {
      var key = keys[i - 1];
      var val = this.page._decodeURLEncodedURIComponent(m[i]);
      if (val !== undefined || !(hasOwnProperty.call(params, key.name))) {
        params[key.name] = val;
      }
    }

    return true;
  };


  /**
   * Module exports.
   */

  var globalPage = createPage();
  var page_js = globalPage;
  var default_1 = globalPage;

page_js.default = default_1;

return page_js;

})));


================================================
FILE: page.mjs
================================================
var isarray = Array.isArray || function (arr) {
  return Object.prototype.toString.call(arr) == '[object Array]';
};

/**
 * Expose `pathToRegexp`.
 */
var pathToRegexp_1 = pathToRegexp;
var parse_1 = parse;
var compile_1 = compile;
var tokensToFunction_1 = tokensToFunction;
var tokensToRegExp_1 = tokensToRegExp;

/**
 * The main path matching regexp utility.
 *
 * @type {RegExp}
 */
var PATH_REGEXP = new RegExp([
  // Match escaped characters that would otherwise appear in future matches.
  // This allows the user to escape special characters that won't transform.
  '(\\\\.)',
  // Match Express-style parameters and un-named parameters with a prefix
  // and optional suffixes. Matches appear as:
  //
  // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
  // "/route(\\d+)"  => [undefined, undefined, undefined, "\d+", undefined, undefined]
  // "/*"            => ["/", undefined, undefined, undefined, undefined, "*"]
  '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^()])+)\\))?|\\(((?:\\\\.|[^()])+)\\))([+*?])?|(\\*))'
].join('|'), 'g');

/**
 * Parse a string for the raw tokens.
 *
 * @param  {String} str
 * @return {Array}
 */
function parse (str) {
  var tokens = [];
  var key = 0;
  var index = 0;
  var path = '';
  var res;

  while ((res = PATH_REGEXP.exec(str)) != null) {
    var m = res[0];
    var escaped = res[1];
    var offset = res.index;
    path += str.slice(index, offset);
    index = offset + m.length;

    // Ignore already escaped sequences.
    if (escaped) {
      path += escaped[1];
      continue
    }

    // Push the current path onto the tokens.
    if (path) {
      tokens.push(path);
      path = '';
    }

    var prefix = res[2];
    var name = res[3];
    var capture = res[4];
    var group = res[5];
    var suffix = res[6];
    var asterisk = res[7];

    var repeat = suffix === '+' || suffix === '*';
    var optional = suffix === '?' || suffix === '*';
    var delimiter = prefix || '/';
    var pattern = capture || group || (asterisk ? '.*' : '[^' + delimiter + ']+?');

    tokens.push({
      name: name || key++,
      prefix: prefix || '',
      delimiter: delimiter,
      optional: optional,
      repeat: repeat,
      pattern: escapeGroup(pattern)
    });
  }

  // Match any characters still remaining.
  if (index < str.length) {
    path += str.substr(index);
  }

  // If the path exists, push it onto the end.
  if (path) {
    tokens.push(path);
  }

  return tokens
}

/**
 * Compile a string to a template function for the path.
 *
 * @param  {String}   str
 * @return {Function}
 */
function compile (str) {
  return tokensToFunction(parse(str))
}

/**
 * Expose a method for transforming tokens into the path function.
 */
function tokensToFunction (tokens) {
  // Compile all the tokens into regexps.
  var matches = new Array(tokens.length);

  // Compile all the patterns before compilation.
  for (var i = 0; i < tokens.length; i++) {
    if (typeof tokens[i] === 'object') {
      matches[i] = new RegExp('^' + tokens[i].pattern + '$');
    }
  }

  return function (obj) {
    var path = '';
    var data = obj || {};

    for (var i = 0; i < tokens.length; i++) {
      var token = tokens[i];

      if (typeof token === 'string') {
        path += token;

        continue
      }

      var value = data[token.name];
      var segment;

      if (value == null) {
        if (token.optional) {
          continue
        } else {
          throw new TypeError('Expected "' + token.name + '" to be defined')
        }
      }

      if (isarray(value)) {
        if (!token.repeat) {
          throw new TypeError('Expected "' + token.name + '" to not repeat, but received "' + value + '"')
        }

        if (value.length === 0) {
          if (token.optional) {
            continue
          } else {
            throw new TypeError('Expected "' + token.name + '" to not be empty')
          }
        }

        for (var j = 0; j < value.length; j++) {
          segment = encodeURIComponent(value[j]);

          if (!matches[i].test(segment)) {
            throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
          }

          path += (j === 0 ? token.prefix : token.delimiter) + segment;
        }

        continue
      }

      segment = encodeURIComponent(value);

      if (!matches[i].test(segment)) {
        throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
      }

      path += token.prefix + segment;
    }

    return path
  }
}

/**
 * Escape a regular expression string.
 *
 * @param  {String} str
 * @return {String}
 */
function escapeString (str) {
  return str.replace(/([.+*?=^!:${}()[\]|\/])/g, '\\$1')
}

/**
 * Escape the capturing group by escaping special characters and meaning.
 *
 * @param  {String} group
 * @return {String}
 */
function escapeGroup (group) {
  return group.replace(/([=!:$\/()])/g, '\\$1')
}

/**
 * Attach the keys as a property of the regexp.
 *
 * @param  {RegExp} re
 * @param  {Array}  keys
 * @return {RegExp}
 */
function attachKeys (re, keys) {
  re.keys = keys;
  return re
}

/**
 * Get the flags for a regexp from the options.
 *
 * @param  {Object} options
 * @return {String}
 */
function flags (options) {
  return options.sensitive ? '' : 'i'
}

/**
 * Pull out keys from a regexp.
 *
 * @param  {RegExp} path
 * @param  {Array}  keys
 * @return {RegExp}
 */
function regexpToRegexp (path, keys) {
  // Use a negative lookahead to match only capturing groups.
  var groups = path.source.match(/\((?!\?)/g);

  if (groups) {
    for (var i = 0; i < groups.length; i++) {
      keys.push({
        name: i,
        prefix: null,
        delimiter: null,
        optional: false,
        repeat: false,
        pattern: null
      });
    }
  }

  return attachKeys(path, keys)
}

/**
 * Transform an array into a regexp.
 *
 * @param  {Array}  path
 * @param  {Array}  keys
 * @param  {Object} options
 * @return {RegExp}
 */
function arrayToRegexp (path, keys, options) {
  var parts = [];

  for (var i = 0; i < path.length; i++) {
    parts.push(pathToRegexp(path[i], keys, options).source);
  }

  var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options));

  return attachKeys(regexp, keys)
}

/**
 * Create a path regexp from string input.
 *
 * @param  {String} path
 * @param  {Array}  keys
 * @param  {Object} options
 * @return {RegExp}
 */
function stringToRegexp (path, keys, options) {
  var tokens = parse(path);
  var re = tokensToRegExp(tokens, options);

  // Attach keys back to the regexp.
  for (var i = 0; i < tokens.length; i++) {
    if (typeof tokens[i] !== 'string') {
      keys.push(tokens[i]);
    }
  }

  return attachKeys(re, keys)
}

/**
 * Expose a function for taking tokens and returning a RegExp.
 *
 * @param  {Array}  tokens
 * @param  {Array}  keys
 * @param  {Object} options
 * @return {RegExp}
 */
function tokensToRegExp (tokens, options) {
  options = options || {};

  var strict = options.strict;
  var end = options.end !== false;
  var route = '';
  var lastToken = tokens[tokens.length - 1];
  var endsWithSlash = typeof lastToken === 'string' && /\/$/.test(lastToken);

  // Iterate over the tokens and create our regexp string.
  for (var i = 0; i < tokens.length; i++) {
    var token = tokens[i];

    if (typeof token === 'string') {
      route += escapeString(token);
    } else {
      var prefix = escapeString(token.prefix);
      var capture = token.pattern;

      if (token.repeat) {
        capture += '(?:' + prefix + capture + ')*';
      }

      if (token.optional) {
        if (prefix) {
          capture = '(?:' + prefix + '(' + capture + '))?';
        } else {
          capture = '(' + capture + ')?';
        }
      } else {
        capture = prefix + '(' + capture + ')';
      }

      route += capture;
    }
  }

  // In non-strict mode we allow a slash at the end of match. If the path to
  // match already ends with a slash, we remove it for consistency. The slash
  // is valid at the end of a path match, not in the middle. This is important
  // in non-ending mode, where "/test/" shouldn't match "/test//route".
  if (!strict) {
    route = (endsWithSlash ? route.slice(0, -2) : route) + '(?:\\/(?=$))?';
  }

  if (end) {
    route += '$';
  } else {
    // In non-ending mode, we need the capturing groups to match as much as
    // possible by using a positive lookahead to the end or next path segment.
    route += strict && endsWithSlash ? '' : '(?=\\/|$)';
  }

  return new RegExp('^' + route, flags(options))
}

/**
 * Normalize the given path string, returning a regular expression.
 *
 * An empty array can be passed in for the keys, which will hold the
 * placeholder key descriptions. For example, using `/user/:id`, `keys` will
 * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
 *
 * @param  {(String|RegExp|Array)} path
 * @param  {Array}                 [keys]
 * @param  {Object}                [options]
 * @return {RegExp}
 */
function pathToRegexp (path, keys, options) {
  keys = keys || [];

  if (!isarray(keys)) {
    options = keys;
    keys = [];
  } else if (!options) {
    options = {};
  }

  if (path instanceof RegExp) {
    return regexpToRegexp(path, keys, options)
  }

  if (isarray(path)) {
    return arrayToRegexp(path, keys, options)
  }

  return stringToRegexp(path, keys, options)
}

pathToRegexp_1.parse = parse_1;
pathToRegexp_1.compile = compile_1;
pathToRegexp_1.tokensToFunction = tokensToFunction_1;
pathToRegexp_1.tokensToRegExp = tokensToRegExp_1;

/**
   * Module dependencies.
   */

  

  /**
   * Short-cuts for global-object checks
   */

  var hasDocument = ('undefined' !== typeof document);
  var hasWindow = ('undefined' !== typeof window);
  var hasHistory = ('undefined' !== typeof history);
  var hasProcess = typeof process !== 'undefined';

  /**
   * Detect click event
   */
  var clickEvent = hasDocument && document.ontouchstart ? 'touchstart' : 'click';

  /**
   * To work properly with the URL
   * history.location generated polyfill in https://github.com/devote/HTML5-History-API
   */

  var isLocation = hasWindow && !!(window.history.location || window.location);

  /**
   * The page instance
   * @api private
   */
  function Page() {
    // public things
    this.callbacks = [];
    this.exits = [];
    this.current = '';
    this.len = 0;

    // private things
    this._decodeURLComponents = true;
    this._base = '';
    this._strict = false;
    this._running = false;
    this._hashbang = false;

    // bound functions
    this.clickHandler = this.clickHandler.bind(this);
    this._onpopstate = this._onpopstate.bind(this);
  }

  /**
   * Configure the instance of page. This can be called multiple times.
   *
   * @param {Object} options
   * @api public
   */

  Page.prototype.configure = function(options) {
    var opts = options || {};

    this._window = opts.window || (hasWindow && window);
    this._decodeURLComponents = opts.decodeURLComponents !== false;
    this._popstate = opts.popstate !== false && hasWindow;
    this._click = opts.click !== false && hasDocument;
    this._hashbang = !!opts.hashbang;

    var _window = this._window;
    if(this._popstate) {
      _window.addEventListener('popstate', this._onpopstate, false);
    } else if(hasWindow) {
      _window.removeEventListener('popstate', this._onpopstate, false);
    }

    if (this._click) {
      _window.document.addEventListener(clickEvent, this.clickHandler, false);
    } else if(hasDocument) {
      _window.document.removeEventListener(clickEvent, this.clickHandler, false);
    }

    if(this._hashbang && hasWindow && !hasHistory) {
      _window.addEventListener('hashchange', this._onpopstate, false);
    } else if(hasWindow) {
      _window.removeEventListener('hashchange', this._onpopstate, false);
    }
  };

  /**
   * Get or set basepath to `path`.
   *
   * @param {string} path
   * @api public
   */

  Page.prototype.base = function(path) {
    if (0 === arguments.length) return this._base;
    this._base = path;
  };

  /**
   * Gets the `base`, which depends on whether we are using History or
   * hashbang routing.

   * @api private
   */
  Page.prototype._getBase = function() {
    var base = this._base;
    if(!!base) return base;
    var loc = hasWindow && this._window && this._window.location;

    if(hasWindow && this._hashbang && loc && loc.protocol === 'file:') {
      base = loc.pathname;
    }

    return base;
  };

  /**
   * Get or set strict path matching to `enable`
   *
   * @param {boolean} enable
   * @api public
   */

  Page.prototype.strict = function(enable) {
    if (0 === arguments.length) return this._strict;
    this._strict = enable;
  };


  /**
   * Bind with the given `options`.
   *
   * Options:
   *
   *    - `click` bind to click events [true]
   *    - `popstate` bind to popstate [true]
   *    - `dispatch` perform initial dispatch [true]
   *
   * @param {Object} options
   * @api public
   */

  Page.prototype.start = function(options) {
    var opts = options || {};
    this.configure(opts);

    if (false === opts.dispatch) return;
    this._running = true;

    var url;
    if(isLocation) {
      var window = this._window;
      var loc = window.location;

      if(this._hashbang && ~loc.hash.indexOf('#!')) {
        url = loc.hash.substr(2) + loc.search;
      } else if (this._hashbang) {
        url = loc.search + loc.hash;
      } else {
        url = loc.pathname + loc.search + loc.hash;
      }
    }

    this.replace(url, null, true, opts.dispatch);
  };

  /**
   * Unbind click and popstate event handlers.
   *
   * @api public
   */

  Page.prototype.stop = function() {
    if (!this._running) return;
    this.current = '';
    this.len = 0;
    this._running = false;

    var window = this._window;
    this._click && window.document.removeEventListener(clickEvent, this.clickHandler, false);
    hasWindow && window.removeEventListener('popstate', this._onpopstate, false);
    hasWindow && window.removeEventListener('hashchange', this._onpopstate, false);
  };

  /**
   * Show `path` with optional `state` object.
   *
   * @param {string} path
   * @param {Object=} state
   * @param {boolean=} dispatch
   * @param {boolean=} push
   * @return {!Context}
   * @api public
   */

  Page.prototype.show = function(path, state, dispatch, push) {
    var ctx = new Context(path, state, this),
      prev = this.prevContext;
    this.prevContext = ctx;
    this.current = ctx.path;
    if (false !== dispatch) this.dispatch(ctx, prev);
    if (false !== ctx.handled && false !== push) ctx.pushState();
    return ctx;
  };

  /**
   * Goes back in the history
   * Back should always let the current route push state and then go back.
   *
   * @param {string} path - fallback path to go back if no more history exists, if undefined defaults to page.base
   * @param {Object=} state
   * @api public
   */

  Page.prototype.back = function(path, state) {
    var page = this;
    if (this.len > 0) {
      var window = this._window;
      // this may need more testing to see if all browsers
      // wait for the next tick to go back in history
      hasHistory && window.history.back();
      this.len--;
    } else if (path) {
      setTimeout(function() {
        page.show(path, state);
      });
    } else {
      setTimeout(function() {
        page.show(page._getBase(), state);
      });
    }
  };

  /**
   * Register route to redirect from one path to other
   * or just redirect to another route
   *
   * @param {string} from - if param 'to' is undefined redirects to 'from'
   * @param {string=} to
   * @api public
   */
  Page.prototype.redirect = function(from, to) {
    var inst = this;

    // Define route from a path to another
    if ('string' === typeof from && 'string' === typeof to) {
      page.call(this, from, function(e) {
        setTimeout(function() {
          inst.replace(/** @type {!string} */ (to));
        }, 0);
      });
    }

    // Wait for the push state and replace it with another
    if ('string' === typeof from && 'undefined' === typeof to) {
      setTimeout(function() {
        inst.replace(from);
      }, 0);
    }
  };

  /**
   * Replace `path` with optional `state` object.
   *
   * @param {string} path
   * @param {Object=} state
   * @param {boolean=} init
   * @param {boolean=} dispatch
   * @return {!Context}
   * @api public
   */


  Page.prototype.replace = function(path, state, init, dispatch) {
    var ctx = new Context(path, state, this),
      prev = this.prevContext;
    this.prevContext = ctx;
    this.current = ctx.path;
    ctx.init = init;
    ctx.save(); // save before dispatching, which may redirect
    if (false !== dispatch) this.dispatch(ctx, prev);
    return ctx;
  };

  /**
   * Dispatch the given `ctx`.
   *
   * @param {Context} ctx
   * @api private
   */

  Page.prototype.dispatch = function(ctx, prev) {
    var i = 0, j = 0, page = this;

    function nextExit() {
      var fn = page.exits[j++];
      if (!fn) return nextEnter();
      fn(prev, nextExit);
    }

    function nextEnter() {
      var fn = page.callbacks[i++];

      if (ctx.path !== page.current) {
        ctx.handled = false;
        return;
      }
      if (!fn) return unhandled.call(page, ctx);
      fn(ctx, nextEnter);
    }

    if (prev) {
      nextExit();
    } else {
      nextEnter();
    }
  };

  /**
   * Register an exit route on `path` with
   * callback `fn()`, which will be called
   * on the previous context when a new
   * page is visited.
   */
  Page.prototype.exit = function(path, fn) {
    if (typeof path === 'function') {
      return this.exit('*', path);
    }

    var route = new Route(path, null, this);
    for (var i = 1; i < arguments.length; ++i) {
      this.exits.push(route.middleware(arguments[i]));
    }
  };

  /**
   * Handle "click" events.
   */

  /* jshint +W054 */
  Page.prototype.clickHandler = function(e) {
    if (1 !== this._which(e)) return;

    if (e.metaKey || e.ctrlKey || e.shiftKey) return;
    if (e.defaultPrevented) return;

    // ensure link
    // use shadow dom when available if not, fall back to composedPath()
    // for browsers that only have shady
    var el = e.target;
    var eventPath = e.path || (e.composedPath ? e.composedPath() : null);

    if(eventPath) {
      for (var i = 0; i < eventPath.length; i++) {
        if (!eventPath[i].nodeName) continue;
        if (eventPath[i].nodeName.toUpperCase() !== 'A') continue;
        if (!eventPath[i].href) continue;

        el = eventPath[i];
        break;
      }
    }

    // continue ensure link
    // el.nodeName for svg links are 'a' instead of 'A'
    while (el && 'A' !== el.nodeName.toUpperCase()) el = el.parentNode;
    if (!el || 'A' !== el.nodeName.toUpperCase()) return;

    // check if link is inside an svg
    // in this case, both href and target are always inside an object
    var svg = (typeof el.href === 'object') && el.href.constructor.name === 'SVGAnimatedString';

    // Ignore if tag has
    // 1. "download" attribute
    // 2. rel="external" attribute
    if (el.hasAttribute('download') || el.getAttribute('rel') === 'external') return;

    // ensure non-hash for the same path
    var link = el.getAttribute('href');
    if(!this._hashbang && this._samePath(el) && (el.hash || '#' === link)) return;

    // Check for mailto: in the href
    if (link && link.indexOf('mailto:') > -1) return;

    // check target
    // svg target is an object and its desired value is in .baseVal property
    if (svg ? el.target.baseVal : el.target) return;

    // x-origin
    // note: svg links that are not relative don't call click events (and skip page.js)
    // consequently, all svg links tested inside page.js are relative and in the same origin
    if (!svg && !this.sameOrigin(el.href)) return;

    // rebuild path
    // There aren't .pathname and .search properties in svg links, so we use href
    // Also, svg href is an object and its desired value is in .baseVal property
    var path = svg ? el.href.baseVal : (el.pathname + el.search + (el.hash || ''));

    path = path[0] !== '/' ? '/' + path : path;

    // strip leading "/[drive letter]:" on NW.js on Windows
    if (hasProcess && path.match(/^\/[a-zA-Z]:\//)) {
      path = path.replace(/^\/[a-zA-Z]:\//, '/');
    }

    // same page
    var orig = path;
    var pageBase = this._getBase();

    if (path.indexOf(pageBase) === 0) {
      path = path.substr(pageBase.length);
    }

    if (this._hashbang) path = path.replace('#!', '');

    if (pageBase && orig === path && (!isLocation || this._window.location.protocol !== 'file:')) {
      return;
    }

    e.preventDefault();
    this.show(orig);
  };

  /**
   * Handle "populate" events.
   * @api private
   */

  Page.prototype._onpopstate = (function () {
    var loaded = false;
    if ( ! hasWindow ) {
      return function () {};
    }
    if (hasDocument && document.readyState === 'complete') {
      loaded = true;
    } else {
      window.addEventListener('load', function() {
        setTimeout(function() {
          loaded = true;
        }, 0);
      });
    }
    return function onpopstate(e) {
      if (!loaded) return;
      var page = this;
      if (e.state) {
        var path = e.state.path;
        page.replace(path, e.state);
      } else if (isLocation) {
        var loc = page._window.location;
        page.show(loc.pathname + loc.search + loc.hash, undefined, undefined, false);
      }
    };
  })();

  /**
   * Event button.
   */
  Page.prototype._which = function(e) {
    e = e || (hasWindow && this._window.event);
    return null == e.which ? e.button : e.which;
  };

  /**
   * Convert to a URL object
   * @api private
   */
  Page.prototype._toURL = function(href) {
    var window = this._window;
    if(typeof URL === 'function' && isLocation) {
      return new URL(href, window.location.toString());
    } else if (hasDocument) {
      var anc = window.document.createElement('a');
      anc.href = href;
      return anc;
    }
  };

  /**
   * Check if `href` is the same origin.
   * @param {string} href
   * @api public
   */
  Page.prototype.sameOrigin = function(href) {
    if(!href || !isLocation) return false;

    var url = this._toURL(href);
    var window = this._window;

    var loc = window.location;

    /*
       When the port is the default http port 80 for http, or 443 for
       https, internet explorer 11 returns an empty string for loc.port,
       so we need to compare loc.port with an empty string if url.port
       is the default port 80 or 443.
       Also the comparition with `port` is changed from `===` to `==` because
       `port` can be a string sometimes. This only applies to ie11.
    */
    return loc.protocol === url.protocol &&
      loc.hostname === url.hostname &&
      (loc.port === url.port || loc.port === '' && (url.port == 80 || url.port == 443)); // jshint ignore:line
  };

  /**
   * @api private
   */
  Page.prototype._samePath = function(url) {
    if(!isLocation) return false;
    var window = this._window;
    var loc = window.location;
    return url.pathname === loc.pathname &&
      url.search === loc.search;
  };

  /**
   * Remove URL encoding from the given `str`.
   * Accommodates whitespace in both x-www-form-urlencoded
   * and regular percent-encoded form.
   *
   * @param {string} val - URL component to decode
   * @api private
   */
  Page.prototype._decodeURLEncodedURIComponent = function(val) {
    if (typeof val !== 'string') { return val; }
    return this._decodeURLComponents ? decodeURIComponent(val.replace(/\+/g, ' ')) : val;
  };

  /**
   * Create a new `page` instance and function
   */
  function createPage() {
    var pageInstance = new Page();

    function pageFn(/* args */) {
      return page.apply(pageInstance, arguments);
    }

    // Copy all of the things over. In 2.0 maybe we use setPrototypeOf
    pageFn.callbacks = pageInstance.callbacks;
    pageFn.exits = pageInstance.exits;
    pageFn.base = pageInstance.base.bind(pageInstance);
    pageFn.strict = pageInstance.strict.bind(pageInstance);
    pageFn.start = pageInstance.start.bind(pageInstance);
    pageFn.stop = pageInstance.stop.bind(pageInstance);
    pageFn.show = pageInstance.show.bind(pageInstance);
    pageFn.back = pageInstance.back.bind(pageInstance);
    pageFn.redirect = pageInstance.redirect.bind(pageInstance);
    pageFn.replace = pageInstance.replace.bind(pageInstance);
    pageFn.dispatch = pageInstance.dispatch.bind(pageInstance);
    pageFn.exit = pageInstance.exit.bind(pageInstance);
    pageFn.configure = pageInstance.configure.bind(pageInstance);
    pageFn.sameOrigin = pageInstance.sameOrigin.bind(pageInstance);
    pageFn.clickHandler = pageInstance.clickHandler.bind(pageInstance);

    pageFn.create = createPage;

    Object.defineProperty(pageFn, 'len', {
      get: function(){
        return pageInstance.len;
      },
      set: function(val) {
        pageInstance.len = val;
      }
    });

    Object.defineProperty(pageFn, 'current', {
      get: function(){
        return pageInstance.current;
      },
      set: function(val) {
        pageInstance.current = val;
      }
    });

    // In 2.0 these can be named exports
    pageFn.Context = Context;
    pageFn.Route = Route;

    return pageFn;
  }

  /**
   * Register `path` with callback `fn()`,
   * or route `path`, or redirection,
   * or `page.start()`.
   *
   *   page(fn);
   *   page('*', fn);
   *   page('/user/:id', load, user);
   *   page('/user/' + user.id, { some: 'thing' });
   *   page('/user/' + user.id);
   *   page('/from', '/to')
   *   page();
   *
   * @param {string|!Function|!Object} path
   * @param {Function=} fn
   * @api public
   */

  function page(path, fn) {
    // <callback>
    if ('function' === typeof path) {
      return page.call(this, '*', path);
    }

    // route <path> to <callback ...>
    if ('function' === typeof fn) {
      var route = new Route(/** @type {string} */ (path), null, this);
      for (var i = 1; i < arguments.length; ++i) {
        this.callbacks.push(route.middleware(arguments[i]));
      }
      // show <path> with [state]
    } else if ('string' === typeof path) {
      this['string' === typeof fn ? 'redirect' : 'show'](path, fn);
      // start [options]
    } else {
      this.start(path);
    }
  }

  /**
   * Unhandled `ctx`. When it's not the initial
   * popstate then redirect. If you wish to handle
   * 404s on your own use `page('*', callback)`.
   *
   * @param {Context} ctx
   * @api private
   */
  function unhandled(ctx) {
    if (ctx.handled) return;
    var current;
    var page = this;
    var window = page._window;

    if (page._hashbang) {
      current = isLocation && this._getBase() + window.location.hash.replace('#!', '');
    } else {
      current = isLocation && window.location.pathname + window.location.search;
    }

    if (current === ctx.canonicalPath) return;
    page.stop();
    ctx.handled = false;
    isLocation && (window.location.href = ctx.canonicalPath);
  }

  /**
   * Escapes RegExp characters in the given string.
   *
   * @param {string} s
   * @api private
   */
  function escapeRegExp(s) {
    return s.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
  }

  /**
   * Initialize a new "request" `Context`
   * with the given `path` and optional initial `state`.
   *
   * @constructor
   * @param {string} path
   * @param {Object=} state
   * @api public
   */

  function Context(path, state, pageInstance) {
    var _page = this.page = pageInstance || page;
    var window = _page._window;
    var hashbang = _page._hashbang;

    var pageBase = _page._getBase();
    if ('/' === path[0] && 0 !== path.indexOf(pageBase)) path = pageBase + (hashbang ? '#!' : '') + path;
    var i = path.indexOf('?');

    this.canonicalPath = path;
    var re = new RegExp('^' + escapeRegExp(pageBase));
    this.path = path.replace(re, '') || '/';
    if (hashbang) this.path = this.path.replace('#!', '') || '/';

    this.title = (hasDocument && window.document.title);
    this.state = state || {};
    this.state.path = path;
    this.querystring = ~i ? _page._decodeURLEncodedURIComponent(path.slice(i + 1)) : '';
    this.pathname = _page._decodeURLEncodedURIComponent(~i ? path.slice(0, i) : path);
    this.params = {};

    // fragment
    this.hash = '';
    if (!hashbang) {
      if (!~this.path.indexOf('#')) return;
      var parts = this.path.split('#');
      this.path = this.pathname = parts[0];
      this.hash = _page._decodeURLEncodedURIComponent(parts[1]) || '';
      this.querystring = this.querystring.split('#')[0];
    }
  }

  /**
   * Push state.
   *
   * @api private
   */

  Context.prototype.pushState = function() {
    var page = this.page;
    var window = page._window;
    var hashbang = page._hashbang;

    page.len++;
    if (hasHistory) {
        window.history.pushState(this.state, this.title,
          hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
    }
  };

  /**
   * Save the context state.
   *
   * @api public
   */

  Context.prototype.save = function() {
    var page = this.page;
    if (hasHistory) {
        page._window.history.replaceState(this.state, this.title,
          page._hashbang && this.path !== '/' ? '#!' + this.path : this.canonicalPath);
    }
  };

  /**
   * Initialize `Route` with the given HTTP `path`,
   * and an array of `callbacks` and `options`.
   *
   * Options:
   *
   *   - `sensitive`    enable case-sensitive routes
   *   - `strict`       enable strict matching for trailing slashes
   *
   * @constructor
   * @param {string} path
   * @param {Object=} options
   * @api private
   */

  function Route(path, options, page) {
    var _page = this.page = page || globalPage;
    var opts = options || {};
    opts.strict = opts.strict || _page._strict;
    this.path = (path === '*') ? '(.*)' : path;
    this.method = 'GET';
    this.regexp = pathToRegexp_1(this.path, this.keys = [], opts);
  }

  /**
   * Return route middleware with
   * the given callback `fn()`.
   *
   * @param {Function} fn
   * @return {Function}
   * @api public
   */

  Route.prototype.middleware = function(fn) {
    var self = this;
    return function(ctx, next) {
      if (self.match(ctx.path, ctx.params)) {
        ctx.routePath = self.path;
        return fn(ctx, next);
      }
      next();
    };
  };

  /**
   * Check if this route matches `path`, if so
   * populate `params`.
   *
   * @param {string} path
   * @param {Object} params
   * @return {boolean}
   * @api private
   */

  Route.prototype.match = function(path, params) {
    var keys = this.keys,
      qsIndex = path.indexOf('?'),
      pathname = ~qsIndex ? path.slice(0, qsIndex) : path,
      m = this.regexp.exec(decodeURIComponent(pathname));

    if (!m) return false;

    delete params[0];

    for (var i = 1, len = m.length; i < len; ++i) {
      var key = keys[i - 1];
      var val = this.page._decodeURLEncodedURIComponent(m[i]);
      if (val !== undefined || !(hasOwnProperty.call(params, key.name))) {
        params[key.name] = val;
      }
    }

    return true;
  };


  /**
   * Module exports.
   */

  var globalPage = createPage();
  var page_js = globalPage;
  var default_1 = globalPage;

page_js.default = default_1;

export default page_js;


================================================
FILE: rollup.config.js
================================================
import commonjs from 'rollup-plugin-commonjs';
import nodeResolve from 'rollup-plugin-node-resolve';

export default {
  input: 'index.js',
  output: {
    file: 'page.js',
    format: 'umd',
    name: 'page'
  },
  plugins: [
    nodeResolve({
      jsnext: true,
      main: true
    }),
    commonjs({
      include: ['node_modules/**', '**']
    })
  ]
};


================================================
FILE: test/demos/back-demo.html
================================================
<!doctype html>
<html lang="en">
<title>Blank page</title>
<body>
  <ul class="links">
    <li><a class="current-page" href="./back-demo.html?key=val#hash">Click me</a></li>
  </ul>

  <script src="../page.js"></script>
  <script>
    page("/test/demos/back-demo.html", function(ctx){
      console.assert(ctx.querystring === "key=val");
    });
  </script>
</body>


================================================
FILE: test/index.html
================================================
<html>
  <head>
    <title>page.js - tests</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" href="https://cdn.rawgit.com/visionmedia/mocha/1.20.1/mocha.css" />
  </head>
  <body>
    <div id="mocha"></div>
    <script src="https://cdn.rawgit.com/visionmedia/mocha/1.20.1/mocha.js"></script>
    <script src="https://cdn.rawgit.com/chaijs/chai/1.9.1/chai.js"></script>
    <script>mocha.setup('bdd')</script>
    <script src="page.js"></script>
    <script src="tests.js"></script>
    <script>
      mocha.run();
    </script>
  </body>
</html>


================================================
FILE: test/mocha.opts
================================================
-R spec


================================================
FILE: test/support/jsdom.js
================================================

function setupJsdom(options, next) {
  options = options || {};

  var jsdom = require('jsdom');
  var html = '<!doctype html><html><head></head><body></body></html>';

  function setupGlobals(window) {
    window.console = console;
    if(window.location.protocol !== 'file:')
      window.history.replaceState(null, '', '/');
    global.window = window;
    global.location = window.location;
    global.document = window.document;
    global.history = window.history;
    window._$jscoverage = global._$jscoverage;
  }

  if(jsdom.env) {
    jsdom.env({
      html: html,
      url: options.url || 'http://example.com',
      done: function(errors, window) {
        setupGlobals(window);
        if (errors) {
          errors.forEach(console.log);
          throw new Error(errors[0].data.error);
        }
        next();
      }
    });
  } else {
    var dom = new jsdom.JSDOM(html, {
      url: options.url || 'http://example.com'
    });
    setupGlobals(dom.window);
    next();
  }
}

exports.setup = setupJsdom;

before(function(next) {
  setupJsdom({}, next);
});


================================================
FILE: test/test-page.html
================================================
<!doctype html>
<html lang="en">
<title>Blank page</title>
<body>
  <ul class="links">
    <li><a class="index" href="./">/</a></li>
    <li><a class="whoop" href="#whoop">#whoop</a></li>
    <li><a class="about" href="./about">/about</a></li>
    <li><a class="query" href="./query?foo=bar">/query?foo=bar</a></li>
    <li><a class="query-hash" href="./query#page">/query#page</a></li>
    <li><a class="link-trailing" href="./link-trailing/">/link-trailing/</a></li>
    <li><a class="link-no-trailing" href="./link-no-trailing">/link-no-trailing</a></li>
    <li><a class="contact" href="./contact">/contact</a></li>
    <li><a class="contact-me" href="./contact/me">/contact/me</a></li>
    <li><a class="not-found" href="./not-found?foo=bar">/not-found</a></li>
    <li><a class="diff-domain" href="http://example.com.uk/diff/domain">another domain</a></li>
    <li><a class="shadow-path" href="./shadow">/shadow</a></li>
  </ul>
</body>


================================================
FILE: test/tests.js
================================================
/* globals before, after, chai, expect, page, describe, it */
(function() {

  'use strict';

  var isNode = typeof window !== 'object',
    global = this,
    called = false,
    baseRoute = Function.prototype, // noop
    html = '',
    base = '',
    setbase = true,
    hashbang = false,
    decodeURLComponents = true,
    chai = this.chai,
    expect = this.expect,
    page = this.page,
    globalPage = this.page,
    baseTag,
    frame,
    $,
    jsdomSupport;

  if (isNode) {
    jsdomSupport = require('./support/jsdom');
  }

  before(function() {
    if (isNode) {
      chai = require('chai');
      expect = chai.expect;
      globalPage = process.env.PAGE_COV ?
        require('../index-cov') : require('../index');
    } else {
      globalPage = window.page;
      expect = chai.expect;
    }

    $ = function(sel) {
      return frame.contentWindow.document.querySelector(sel);
    };

  });

  var fireEvent = function(node, eventName, path) {
      var event;

      if(typeof testWindow().Event === 'function') {
        var MouseEvent = testWindow().MouseEvent;
        event = new MouseEvent(eventName, {
          bubbles: true,
          button: 1
        });
        Object.defineProperty(event, 'which', { value: null });
      } else {
        event = testWindow().document.createEvent('MouseEvents');

        // https://developer.mozilla.org/en-US/docs/Web/API/event.initMouseEvent
        event.initEvent(
          eventName, true, true, this, 0,
          event.screenX, event.screenY, event.clientX, event.clientY,
          false, false, false, false,
          0, null);

        event.button = 1;
        event.which = null;
      }

      if(path) {
        Object.defineProperty(event, 'path', {
          value: path
        });
      }

      node.dispatchEvent(event);
    },
    testWindow = function(){
      return frame.contentWindow;
    },
    beforeTests = function(options, done) {
      options = options || {};
      page = globalPage.create();

      page('/', function(ctx) {
        called = true;
        baseRoute(ctx);
      });

      function onFrameLoad(){
        if(setbase) {
          if(options.base)
            page.base(options.base);
          var baseTag = frame.contentWindow.document.createElement('base');
          frame.contentWindow.document.head.appendChild(baseTag);

          baseTag.setAttribute('href', (base ? base + '/' : '/'));
        }

        options.window = frame.contentWindow;
        if(options.strict != null)
          page.strict(options.strict);
        page.start(options);
        page(base ? base + '/' : '/');
        done();
      }

      frame = document.createElement('iframe');
      document.body.appendChild(frame);
      if(isNode) {
        var cntn = require('fs').readFileSync(__dirname + '/test-page.html', 'utf8');
        cntn = cntn.replace('<!doctype html>', '').trim();
        cntn = cntn.replace('<html lang="en">', '');
        frame.contentWindow.document.documentElement.innerHTML = cntn;
        onFrameLoad();
      } else {
        frame.src = './test-page.html';
        frame.addEventListener('load', onFrameLoad);
      }
    },
    replaceable = function(route) {
      function realCallback(ctx) {
        obj.callback(ctx);
      }

      var obj = {
        callback: Function.prototype,
        replace: function(cb){
          obj.callback = cb;
        },
        once: function(cb){
          obj.replace(function(ctx){
            obj.callback = Function.prototype;
            cb(ctx);
          });
        }
      };

      page(route, realCallback);

      return obj;
    },
    tests = function() {
      describe('on page load', function() {
        it('should invoke the matching callback', function() {
          expect(called).to.equal(true);
        });

        it('should invoke the matching async callbacks', function(done) {
          page('/async', function(ctx, next) {
            setTimeout(function() {
              next();
            }, 10);
          }, function(ctx, next) {
            setTimeout(function() {
              done();
            }, 10);
          });
          page('/async');
        });
      });

      describe('on redirect', function() {
        it('should load destination page', function(done) {
          page.redirect('/from', '/to');
          page('/to', function() {
            done();
          });
          page('/from');
        });
        it('should work with short alias', function(done) {
          page('/one', '/two');
          page('/two', function() {
            done();
          });
          page('/one');
        });
        it('should load done within redirect', function(done) {
          page('/redirect', function() {
            page.redirect('/done');
          });
          page('/done', function() {
            done();
          });
          page('/redirect');
        });
      });

      describe('on exit', function() {
        it('should run when exiting the page', function(done) {
          var visited = false;
          page('/exit', function() {
            visited = true;
          });

          page.exit('/exit', function() {
            expect(visited).to.equal(true);
            done();
          });

          page('/exit');
          page('/');
        });

        it('should run async callbacks when exiting the page', function(done) {
          var visited = false;
          page('/async-exit', function() {
            visited = true;
          });

          page.exit('/async-exit', function(ctx, next) {
            setTimeout(function() {
              next();
            }, 10);
          }, function(ctx, next) {
            setTimeout(function () {
              expect(visited).to.equal(true);
              done();
            }, 10);
          });

          page('/async-exit');
          page('/');
        });

        it('should only run on matched routes', function(done) {
          page('/should-exit', function() {});
          page('/', function() {});

          page.exit('/should-not-exit', function() {
            throw new Error('This exit route should not have been called');
          });

          page.exit('/should-exit', function() {
            done();
          });

          page('/should-exit');
          page('/');
        });

        it('should use the previous context', function(done) {
          var unique;

          page('/', function() {});
          page('/bootstrap', function(ctx) {
            unique = ctx.unique = {};
          });

          page.exit('/bootstrap', function(ctx) {
            expect(ctx.unique).to.equal(unique);
            done();
          });

          page('/bootstrap');
          page('/');
        });
      });

      describe('no dispatch', function() {
        it('should use the previous context when not dispatching', function(done) {
          var count = 0;

          page('/', function() {});

          page.exit(function(context) {
            var path = context.path;
            setTimeout( function() {
              expect(path).to.equal('/');
              page.replace( '/', null, false, false);
              if ( count === 2 ) {
                done();
                return;
              }
              count++;
            }, 0);
          });

          page('/');

          page('/bootstrap');

          setTimeout( function() {
            page('/bootstrap');
          }, 0 );
        });


        after(function() {
          // remove exit handler that was added
          page.exits.pop();
        });
      });

      describe('page.back', function() {
        var first;

        before(function() {
          first = replaceable('/first', function(){});
          page('/second', function() {});
          page('/first');
          page('/second');
        });

        it('should move back to history', function(done) {
          first.once(function(){
            var path = hashbang
              ? testWindow().location.hash.replace('#!', '')
              : testWindow().location.pathname;
            path = path.replace(base, '');
            expect(path).to.be.equal('/first');
            done();
          });

          page.back('/first');

        });

        it('should decrement page.len on back()', function() {
          var lenAtFirst = page.len;
          page('/second');
          page.back('/first');
          expect(page.len).to.be.equal(lenAtFirst);
        });

        it('calling back() when there is nothing in the history should go to the given path', function(done){
          page('/fourth', function(){
            expect(page.len).to.be.equal(0);
            done();
          });
          page.len = 0;
          page.back('/fourth');
        });

        it('calling back with nothing in the history and no path should go to the base', function(done){
          baseRoute = function(){
            expect(page.len).to.be.equal(0);
            baseRoute = Function.prototype;
            done();
          };
          page.len = 0;
          page.back();
        });
      });

      describe('ctx.querystring', function() {
        it('should default to ""', function(done) {
          page('/querystring-default', function(ctx) {
            expect(ctx.querystring).to.equal('');
            done();
          });

          page('/querystring-default');
        });

        it('should expose the query string', function(done) {
          page('/querystring', function(ctx) {
            expect(ctx.querystring).to.equal('hello=there');
            done();
          });

          page('/querystring?hello=there');
        });

        it('should accommodate URL encoding', function(done) {
          page('/whatever', function(ctx) {
            var expected = decodeURLComponents
              ? 'queryParam=string with whitespace'
              : 'queryParam=string%20with%20whitespace';
            expect(ctx.querystring).to.equal(expected);
            done();
          });

          page('/whatever?queryParam=string%20with%20whitespace');
        });
      });

      describe('ctx.pathname', function() {
        it('should default to ctx.path', function(done) {
          page('/pathname-default', function(ctx) {
            expect(ctx.pathname).to.equal(base + (base && hashbang ? '#!' : '') + '/pathname-default');
            done();
          });

          page('/pathname-default');
        });

        it('should omit the query string', function(done) {
          page('/pathname', function(ctx) {
            expect(ctx.pathname).to.equal(base + (base && hashbang ? '#!' : '') + '/pathname');
            done();
          });

          page('/pathname?hello=there');
        });

        it('should accommodate URL encoding', function(done) {
          page('/long path with whitespace', function(ctx) {
            expect(ctx.pathname).to.equal(base + (base && hashbang ? '#!' : '') +
              (decodeURLComponents ? '/long path with whitespace' : '/long%20path%20with%20whitespace'));
            done();
          });

          page('/long%20path%20with%20whitespace');
        });
      });

      describe('ctx.params', function() {
        it('should always be URL-decoded', function(done) {
          page('/whatever/:param', function(ctx) {
            expect(ctx.params.param).to.equal('param with whitespace');
            done();
          });

          page('/whatever/param%20with%20whitespace');
        });

        it('should be an object', function(done) {
          page('/ctxparams/:param/', function(ctx) {
            expect(ctx.params).to.not.be.an('array');
            expect(ctx.params).to.be.an('object');
            done();
          });
          page('/ctxparams/test/');
        });
        
        it('should handle optional first param', function(done) {
          page(/^\/ctxparams\/(option1|option2)?$/, function(ctx) {
            expect(ctx.params[0]).to.be.undefined;
            done();
          });
          page('/ctxparams/');
        });

        it('should have the original routePath', function(done) {
          page('/route/:param', function(ctx) {
            expect(ctx.routePath).to.equal('/route/:param');
            done();
          });
          page('/route/value');
        });
      });

      describe('ctx.handled', function() {
        it('should skip unhandled redirect if exists', function() {
          page('/page/:page', function(ctx, next) {
            ctx.handled = true;
            next();
          });
          var ctx = page.show('/page/1');
          expect(ctx.handled).to.be.ok;
        });
      });

      describe('links dispatcher', function() {
        it('should invoke the callback', function(done) {
          page('/about', function() {
            done();
          });

          fireEvent($('.about'), 'click');
        });

        it('should handle trailing slashes in URL', function(done) {
          page('/link-trailing', function() {
            expect(page.strict()).to.equal(false);
            done();
          });
          page('/link-trailing/', function() {
            expect(page.strict()).to.equal(true);
            done();
          });
          fireEvent($('.link-trailing'), 'click');
        });

        it('should handle trailing slashes in route', function(done) {
          page('/link-no-trailing/', function() {
            expect(page.strict()).to.equal(false);
            done();
          });
          page('/link-no-trailing', function() {
            expect(page.strict()).to.equal(true);
            done();
          });
          fireEvent($('.link-no-trailing'), 'click');
        });

        it('should invoke the callback with the right params', function(done) {
          page('/contact/:name', function(ctx) {
            expect(ctx.params.name).to.equal('me');
            done();
          });
          fireEvent($('.contact-me'), 'click');
        });

        it('should not invoke the callback', function() {
          page('/whoop', function(ctx) {
            expect(true).to.equal(false);
          });
          fireEvent($('.whoop'), 'click');
        });

        it('should not fire when navigating to a different domain', function(done){
          page('/diff-domain', function(ctx){
            expect(true).to.equal(false);
          });

          testWindow().document.addEventListener('click', function onDocClick(ev){
            ev.preventDefault();
            testWindow().document.removeEventListener('click', onDocClick);
            done();
          });

          fireEvent($('.diff-domain'), 'click');
        });

        it('works with shadow paths', function() {
          page('/shadow', function() {
            expect(true).to.equal(true);
            page('/');
          });

          fireEvent($('.shadow-path'), 'click', [$('.shadow-path')]);
        });
      });


      describe('dispatcher', function() {
        it('should ignore query strings', function(done) {
          page('/qs', function(ctx) {
            done();
          });

          page('/qs?test=true');
        });

        it('should ignore query strings with params', function(done) {
          page('/qs/:name', function(ctx) {
            expect(ctx.params.name).to.equal('tobi');
            done();
          });

          page('/qs/tobi?test=true');
        });

        it('should invoke the matching callback', function(done) {
          page('/user/:name', function(ctx) {
            done();
          });

          page('/user/tj');
        });

        it('should handle trailing slashes in path', function(done) {
          page('/no-trailing', function() {
            expect(page.strict()).to.equal(false);
            done();
          });
          page('/no-trailing/', function() {
            expect(page.strict()).to.equal(true);
            done();
          });
          page('/no-trailing/');
        });

        it('should handle trailing slashes in route', function(done) {
          page('/trailing/', function() {
            expect(page.strict()).to.equal(false);
            done();
          });
          page('/trailing', function() {
            expect(page.strict()).to.equal(true);
            done();
          });
          page('/trailing');
        });

        it('should populate ctx.params', function(done) {
          page('/blog/post/:name', function(ctx) {
            expect(ctx.params.name).to.equal('something');
            done();
          });

          page('/blog/post/something');
        });

        it('should not include hash in ctx.pathname', function(done){
          page('/contact', function(ctx){
            expect(ctx.pathname).to.equal('/contact');
            done();
          });

          page(hashbang ? '/contact' : '/contact#bang');
        });

        describe('when next() is invoked', function() {
          it('should invoke subsequent matching middleware', function(done) {

            var visistedFirst = false;
            page('/forum/*', function(ctx, next) {
              visistedFirst = true;
              next();
            });

            page('/forum/:fid/thread/:tid', function(ctx) {
              expect(visistedFirst).to.equal(true);
              done();
            });

            page('/forum/1/thread/2');
          });
        });

        describe('not found', function() {
          it('should invoke the not found callback', function(done) {
            page(function() {
              done();
            });
            page('/whathever');
          });
        });
      });
    },
    afterTests = function() {
      called = false;
      page.stop();
      page.base('');
      page.strict(false);
      //page('/');
      base = '';
      baseRoute = Function.prototype; // noop
      setbase = true;
      decodeURLComponents = true;
      document.body.removeChild(frame);
    };

  describe('Html5 history navigation', function() {

    before(function(done) {
      beforeTests(null, done);
    });

    tests();

    it('Should dispatch when going to a hash on same path', function(done){
      var cnt = 0;
      page('/query', function(){
        cnt++;
        if(cnt === 2) {
          done();
        }
      });

      fireEvent($('.query'), 'click');
      fireEvent($('.query-hash'), 'click');
    });

    after(function() {
      afterTests();
    });

  });

  describe('Configuration', function() {
    before(function(done) {
      beforeTests(null, done);
    });

    it('Can disable popstate', function() {
      page.configure({ popstate: false });
    });

    it('Can disable click handler', function() {
      page.configure({ click: false });
    });

    after(function() {
      afterTests();
    });
  });

  describe('Hashbang option enabled', function() {

    before(function(done) {
      hashbang = true;
      beforeTests({
        hashbang: hashbang
      }, done);
    });

    tests();

    it('Using hashbang, url\'s pathname not included in path', function(done){
      page.stop();
      baseRoute = function(ctx){
        expect(ctx.path).to.equal('/');
        done();
      };
      page({ hashbang: true, window: frame.contentWindow });
    });

    after(function() {
      hashbang = false;
      afterTests();
    });

  });

  describe('Different Base', function() {

    before(function(done) {
      base = '/newBase';
      beforeTests({
        base: '/newBase'
      }, done);
    });

    tests();

    after(function() {
      afterTests();
    });

  });

  describe('URL path component decoding disabled', function() {
    before(function(done) {
      decodeURLComponents = false;
      beforeTests({
        decodeURLComponents: decodeURLComponents
      }, done);
    });

    tests();

    after(function() {
      afterTests();
    });
  });

  describe('Strict path matching enabled', function() {
    before(function(done) {
      beforeTests({
        strict: true
      }, done);
    });

    tests();

    after(function() {
      afterTests();
    });
  });

  describe('.clickHandler', function() {
    it('is exported by the global page', function() {
      expect(typeof page.clickHandler).to.equal('function');
    });
  });

  describe('Environments without the URL constructor', function() {
    var URLC;
    before(function(done) {
      URLC = global.URL;
      global.URL = undefined;
      beforeTests(null, done);
    });

    tests();

    after(function() {
      global.URL = URLC;
      afterTests();
    });
  });

  var describei = jsdomSupport ? describe : describe.skip;

  describei('File protocol', function() {
    before(function(done){
      jsdomSupport.setup({
        url: 'file:///var/html/index.html'
      }, Function.prototype);

      setbase = false;
      hashbang = true;
      beforeTests({
        hashbang: hashbang
      }, done);
    });

    after(function(){
      hashbang = false;
    });

    it('simple route call', function(){
      page('/about', function(ctx){
        expect(ctx.path).to.equal('/about');
      });
      page('/about');
    });
  });

  describe('Route', function() {
    before(function(done) {
      beforeTests(null, done);
    });

    it('Can create Route', function() {
      var r = new page.Route('/');
    });

    after(function() {
      afterTests();
    });
  });

}).call(this);
Download .txt
gitextract_171_6o_z/

├── .gitignore
├── .jshintrc
├── .travis.yml
├── History.md
├── Makefile
├── Readme.md
├── bower.json
├── component.json
├── examples/
│   ├── album/
│   │   ├── app.js
│   │   ├── index.html
│   │   └── style.css
│   ├── basic/
│   │   └── index.html
│   ├── chrome/
│   │   ├── chrome.css
│   │   └── index.html
│   ├── enterprisejs/
│   │   └── index.html
│   ├── hash/
│   │   └── index.html
│   ├── hashbang/
│   │   └── index.html
│   ├── index.js
│   ├── list.jade
│   ├── notfound/
│   │   └── index.html
│   ├── partials/
│   │   ├── app.js
│   │   ├── index.html
│   │   ├── public/
│   │   │   ├── css/
│   │   │   │   └── style.css
│   │   │   └── js/
│   │   │       └── events.js
│   │   ├── routes/
│   │   │   └── index.js
│   │   ├── test/
│   │   │   ├── index.html
│   │   │   └── tests.js
│   │   └── views/
│   │       ├── about.html
│   │       ├── content.html
│   │       ├── home.html
│   │       └── portfolio.html
│   ├── profile/
│   │   ├── app.js
│   │   ├── index.html
│   │   └── style.css
│   ├── query-string/
│   │   ├── app.js
│   │   ├── index.html
│   │   ├── query.js
│   │   └── style.css
│   ├── server/
│   │   ├── app.js
│   │   ├── index.html
│   │   └── style.css
│   ├── state/
│   │   ├── app.js
│   │   ├── index.html
│   │   └── style.css
│   └── transitions/
│       ├── app.js
│       ├── index.html
│       └── style.css
├── index.js
├── package.json
├── page.js
├── page.mjs
├── rollup.config.js
└── test/
    ├── demos/
    │   └── back-demo.html
    ├── index.html
    ├── mocha.opts
    ├── support/
    │   └── jsdom.js
    ├── test-page.html
    └── tests.js
Download .txt
SYMBOL INDEX (85 symbols across 13 files)

FILE: examples/album/app.js
  function photos (line 11) | function photos(ctx) {
  function notfound (line 21) | function notfound() {
  function display (line 26) | function display(photos) {
  function adjustPager (line 36) | function adjustPager(page) {

FILE: examples/partials/routes/index.js
  function get (line 7) | function get (url, cb) {

FILE: examples/partials/test/tests.js
  function expected (line 15) | function expected (index, idx, header) {

FILE: examples/profile/app.js
  function text (line 37) | function text(str) {
  function displayIndexAfter (line 41) | function displayIndexAfter(ms) {
  function index (line 55) | function index() {
  function load (line 61) | function load(ctx, next) {
  function show (line 66) | function show(ctx) {
  function notfound (line 73) | function notfound() {

FILE: examples/query-string/app.js
  function parse (line 7) | function parse(ctx, next) {
  function show (line 12) | function show(ctx) {

FILE: examples/query-string/query.js
  function promote (line 21) | function promote(parent, key) {
  function parse (line 29) | function parse(parts, parent, key, val) {
  function merge (line 70) | function merge(parent, key, val){
  function parseObject (line 93) | function parseObject(obj){
  function parseString (line 105) | function parseString(str){
  function stringifyString (line 172) | function stringifyString(str, prefix) {
  function stringifyArray (line 186) | function stringifyArray(arr, prefix) {
  function stringifyObject (line 204) | function stringifyObject(obj, prefix) {
  function set (line 230) | function set(obj, key, val) {
  function lastBraceInKey (line 249) | function lastBraceInKey(str) {

FILE: examples/server/app.js
  function index (line 7) | function index() {
  function contact (line 12) | function contact() {

FILE: examples/state/app.js
  function text (line 17) | function text(str) {
  function index (line 21) | function index() {
  function load (line 27) | function load(ctx, next) {
  function show (line 47) | function show(ctx) {
  function notfound (line 54) | function notfound() {

FILE: index.js
  function Page (line 36) | function Page() {
  function nextExit (line 299) | function nextExit() {
  function nextEnter (line 305) | function nextEnter() {
  function createPage (line 536) | function createPage() {
  function page (line 605) | function page(path, fn) {
  function unhandled (line 634) | function unhandled(ctx) {
  function escapeRegExp (line 658) | function escapeRegExp(s) {
  function Context (line 672) | function Context(path, state, pageInstance) {
  function Route (line 751) | function Route(path, options, page) {

FILE: page.js
  function parse (line 44) | function parse (str) {
  function compile (line 111) | function compile (str) {
  function tokensToFunction (line 118) | function tokensToFunction (tokens) {
  function escapeString (line 198) | function escapeString (str) {
  function escapeGroup (line 208) | function escapeGroup (group) {
  function attachKeys (line 219) | function attachKeys (re, keys) {
  function flags (line 230) | function flags (options) {
  function regexpToRegexp (line 241) | function regexpToRegexp (path, keys) {
  function arrayToRegexp (line 269) | function arrayToRegexp (path, keys, options) {
  function stringToRegexp (line 289) | function stringToRegexp (path, keys, options) {
  function tokensToRegExp (line 311) | function tokensToRegExp (tokens, options) {
  function pathToRegexp (line 379) | function pathToRegexp (path, keys, options) {
  function Page (line 436) | function Page() {
  function nextExit (line 699) | function nextExit() {
  function nextEnter (line 705) | function nextEnter() {
  function createPage (line 936) | function createPage() {
  function page (line 1005) | function page(path, fn) {
  function unhandled (line 1034) | function unhandled(ctx) {
  function escapeRegExp (line 1058) | function escapeRegExp(s) {
  function Context (line 1072) | function Context(path, state, pageInstance) {
  function Route (line 1151) | function Route(path, options, page) {

FILE: page.mjs
  function parse (line 38) | function parse (str) {
  function compile (line 105) | function compile (str) {
  function tokensToFunction (line 112) | function tokensToFunction (tokens) {
  function escapeString (line 192) | function escapeString (str) {
  function escapeGroup (line 202) | function escapeGroup (group) {
  function attachKeys (line 213) | function attachKeys (re, keys) {
  function flags (line 224) | function flags (options) {
  function regexpToRegexp (line 235) | function regexpToRegexp (path, keys) {
  function arrayToRegexp (line 263) | function arrayToRegexp (path, keys, options) {
  function stringToRegexp (line 283) | function stringToRegexp (path, keys, options) {
  function tokensToRegExp (line 305) | function tokensToRegExp (tokens, options) {
  function pathToRegexp (line 373) | function pathToRegexp (path, keys, options) {
  function Page (line 430) | function Page() {
  function nextExit (line 693) | function nextExit() {
  function nextEnter (line 699) | function nextEnter() {
  function createPage (line 930) | function createPage() {
  function page (line 999) | function page(path, fn) {
  function unhandled (line 1028) | function unhandled(ctx) {
  function escapeRegExp (line 1052) | function escapeRegExp(s) {
  function Context (line 1066) | function Context(path, state, pageInstance) {
  function Route (line 1145) | function Route(path, options, page) {

FILE: test/support/jsdom.js
  function setupJsdom (line 2) | function setupJsdom(options, next) {

FILE: test/tests.js
  function onFrameLoad (line 89) | function onFrameLoad(){
  function realCallback (line 121) | function realCallback(ctx) {
Condensed preview — 58 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (189K chars).
[
  {
    "path": ".gitignore",
    "chars": 98,
    "preview": "node_modules\ntesting\n.jshint\ncoverage.html\nnpm-debug.log\nindex-cov.js\n.DS_Store\npackage-lock.json\n"
  },
  {
    "path": ".jshintrc",
    "chars": 426,
    "preview": "{\n  \"browser\": true,\n  \"node\":true,\n  \"expr\": true,\n  \"eqnull\": true,\n  \"laxcomma\": true,\n  \"-W079\": true,\n  \"-W014\": tr"
  },
  {
    "path": ".travis.yml",
    "chars": 290,
    "preview": "language: node_js\nnode_js:\n  - 0.10\n  - node\nbefore_script:\n  - npm run engine-deps\nafter_success:\n  - ./node_modules/.b"
  },
  {
    "path": "History.md",
    "chars": 8708,
    "preview": "1.8.3 / 2018-01-22\n==================\n\nThis is a patch release which switches the build to rollup. Switching shaves 2k o"
  },
  {
    "path": "Makefile",
    "chars": 391,
    "preview": "ROLLUP=node_modules/.bin/rollup\nINFOLOG := \\033[34m ▸\\033[0m\n\nall: page.js page.mjs\n.PHONY: all\n\npage.js: index.js\n\t@ech"
  },
  {
    "path": "Readme.md",
    "chars": 18479,
    "preview": " ![page router logo](http://f.cl.ly/items/3i3n001d0s1Q031r2q1P/page.png)\n\nTiny Express-inspired client-side router.\n\n [!"
  },
  {
    "path": "bower.json",
    "chars": 458,
    "preview": "{\n  \"name\": \"page\",\n  \"description\": \"Tiny client-side router\",\n  \"keywords\": [\"page\", \"route\", \"router\", \"routes\", \"pus"
  },
  {
    "path": "component.json",
    "chars": 298,
    "preview": "{\n  \"name\": \"page\",\n  \"repo\": \"visionmedia/page.js\",\n  \"version\": \"1.6.4\",\n  \"description\": \"Tiny client-side router\",\n "
  },
  {
    "path": "examples/album/app.js",
    "chars": 4463,
    "preview": "var perPage = 6\n  , prev = document.querySelector('#prev')\n  , next = document.querySelector('#next');\n\npage.base('/albu"
  },
  {
    "path": "examples/album/index.html",
    "chars": 493,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Album</title>\n    <script src=\"/page.js\"></script>\n    <link rel=\"stylesheet\""
  },
  {
    "path": "examples/album/style.css",
    "chars": 256,
    "preview": "body {\n  padding: 50px;\n  font: 200 16px \"Helvetica Neue\", Helvetica, Arial;\n}\n\nh1 {\n  font-weight: 300;\n}\n\na {\n  color:"
  },
  {
    "path": "examples/basic/index.html",
    "chars": 1312,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Basic</title>\n    <script src=\"/page.js\"></script>\n    <base href=\"/basic/\" >"
  },
  {
    "path": "examples/chrome/chrome.css",
    "chars": 1171,
    "preview": "\n* {\n  -webkit-box-sizing: border-box;\n  box-sizing: border-box;\n}\n\naside {\n  float: left;\n  width: 120px;\n}\n\nbody {\n  m"
  },
  {
    "path": "examples/chrome/index.html",
    "chars": 2748,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Basic</title>\n    <link rel=\"stylesheet\" href=\"/chrome/chrome.css\" type=\"text"
  },
  {
    "path": "examples/enterprisejs/index.html",
    "chars": 1231,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>EnterpriseJS</title>\n    <script src=\"/page.js\"></script>\n  </head>\n  <body>\n"
  },
  {
    "path": "examples/hash/index.html",
    "chars": 1353,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Hash</title>\n    <style>\n      body p {\n        color: #333;\n        font-fam"
  },
  {
    "path": "examples/hashbang/index.html",
    "chars": 1111,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Hashbang</title>\n    <script src=\"/page.js\"></script>\n    <base href=\"/hashba"
  },
  {
    "path": "examples/index.js",
    "chars": 1617,
    "preview": "\n/**\n * Module dependencies.\n */\n\nvar express = require('express')\n  , join = require('path').join\n  , fs = require('fs'"
  },
  {
    "path": "examples/list.jade",
    "chars": 389,
    "preview": "doctype html\nhtml\n  head\n    title page.js examples\n  body\n    h1 Examples\n    ul\n      for name in examples\n        li\n"
  },
  {
    "path": "examples/notfound/index.html",
    "chars": 1008,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Not Found</title>\n    <script src=\"/page.js\"></script>\n  </head>\n  <body>\n   "
  },
  {
    "path": "examples/partials/app.js",
    "chars": 313,
    "preview": "\npage.base(location.pathname.replace('/partials/', ''));\npage('*', init.ctx);\npage('/partials/home', route.home);\npage('"
  },
  {
    "path": "examples/partials/index.html",
    "chars": 1201,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n  <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />\n  <title>page.js p"
  },
  {
    "path": "examples/partials/public/css/style.css",
    "chars": 562,
    "preview": "* { margin: 0; padding: 0; outline: 0; }\n\nbody { font-family: Arial, Helvetica, Sans-Serif; }\n\n\n.shell { width: 800px; m"
  },
  {
    "path": "examples/partials/public/js/events.js",
    "chars": 268,
    "preview": "\n$(function () {\n  var navLinks = $('#navigation li');\n\n  $('#navigation a').on('click', function (e) {\t\n    changeActiv"
  },
  {
    "path": "examples/partials/routes/index.js",
    "chars": 1728,
    "preview": "\n(function () {\n  // private api\n\n  var cache = {};\n\n  function get (url, cb) {\n    if (cache[url]) return cb(cache[url]"
  },
  {
    "path": "examples/partials/test/index.html",
    "chars": 728,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n  <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />\n  <title>page.js p"
  },
  {
    "path": "examples/partials/test/tests.js",
    "chars": 1246,
    "preview": "\nvar expect = chai.expect;\nmocha.setup({\n  ui: 'bdd',\n  globals: ['']\n});\n\nvar site = null;\nwindow.addEventListener('loa"
  },
  {
    "path": "examples/partials/views/about.html",
    "chars": 36,
    "preview": "<h1>About</h1>\n<p>page {{index}}</p>"
  },
  {
    "path": "examples/partials/views/content.html",
    "chars": 12,
    "preview": "{{>content}}"
  },
  {
    "path": "examples/partials/views/home.html",
    "chars": 35,
    "preview": "<h1>Home</h1>\n<p>page {{index}}</p>"
  },
  {
    "path": "examples/partials/views/portfolio.html",
    "chars": 40,
    "preview": "<h1>Portfolio</h1>\n<p>page {{index}}</p>"
  },
  {
    "path": "examples/profile/app.js",
    "chars": 1685,
    "preview": "\nvar avatars = {\n  glottis: 'http://homepage.ntlworld.com/stureek/images/glottis03.jpg',\n  manny: 'http://kprojekt.net/w"
  },
  {
    "path": "examples/profile/index.html",
    "chars": 525,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Profile</title>\n    <script src=\"/page.js\"></script>\n    <link rel=\"styleshee"
  },
  {
    "path": "examples/profile/style.css",
    "chars": 199,
    "preview": "body {\n  padding: 50px;\n  font: 200 16px \"Helvetica Neue\", Helvetica, Arial;\n}\n\nh1 {\n  font-weight: 300;\n}\n\na {\n  color:"
  },
  {
    "path": "examples/query-string/app.js",
    "chars": 324,
    "preview": "\npage.base('/query-string');\npage('*', parse)\npage('/', show)\npage()\n\nfunction parse(ctx, next) {\n  ctx.query = qs.parse"
  },
  {
    "path": "examples/query-string/index.html",
    "chars": 383,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Query string</title>\n    <link rel=\"stylesheet\" href=\"/query-string/style.css"
  },
  {
    "path": "examples/query-string/query.js",
    "chars": 5457,
    "preview": "(function(exports){\n\n/**\n * Library version.\n */\n\nexports.version = '0.4.2';\n\n/**\n * Object#toString() ref for stringify"
  },
  {
    "path": "examples/query-string/style.css",
    "chars": 199,
    "preview": "body {\n  padding: 50px;\n  font: 200 16px \"Helvetica Neue\", Helvetica, Arial;\n}\n\nh1 {\n  font-weight: 300;\n}\n\na {\n  color:"
  },
  {
    "path": "examples/server/app.js",
    "chars": 315,
    "preview": "\npage.base('/server');\npage('/', index)\npage('/contact', contact)\npage({ dispatch: false })\n\nfunction index() {\n  docume"
  },
  {
    "path": "examples/server/index.html",
    "chars": 351,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Server</title>\n    <script src=\"/page.js\"></script>\n  </head>\n  <body>\n    <h"
  },
  {
    "path": "examples/server/style.css",
    "chars": 200,
    "preview": "body {\n  padding: 50px;\n  font: 200 16px \"Helvetica Neue\", Helvetica, Arial;\n}\n\nh1 {\n  font-weight: 300;\n}\n\na {\n  color:"
  },
  {
    "path": "examples/state/app.js",
    "chars": 1383,
    "preview": "\nvar avatars = {\n  glottis: 'http://homepage.ntlworld.com/stureek/images/glottis03.jpg',\n  manny: 'http://kprojekt.net/w"
  },
  {
    "path": "examples/state/index.html",
    "chars": 464,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Profile</title>\n    <script src=\"/page.js\"></script>\n    <link rel=\"styleshee"
  },
  {
    "path": "examples/state/style.css",
    "chars": 200,
    "preview": "body {\n  padding: 50px;\n  font: 200 16px \"Helvetica Neue\", Helvetica, Arial;\n}\n\nh1 {\n  font-weight: 300;\n}\n\na {\n  color:"
  },
  {
    "path": "examples/transitions/app.js",
    "chars": 631,
    "preview": "\n// content\n\nvar content = document.querySelector('#content');\n\n// current page indicator\n\nvar p = document.querySelecto"
  },
  {
    "path": "examples/transitions/index.html",
    "chars": 538,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Page.js - transitions</title>\n    <script src=\"/page.js\"></script>\n    <link "
  },
  {
    "path": "examples/transitions/style.css",
    "chars": 1063,
    "preview": "\nbody {\n  background: url(/transitions/bg.png);\n  text-align: center;\n  font: 200 14px \"Helvetica Neue\", Helvetica, Aria"
  },
  {
    "path": "index.js",
    "chars": 21910,
    "preview": "  /* globals require, module */\n\n  'use strict';\n\n  /**\n   * Module dependencies.\n   */\n\n  var pathtoRegexp = require('p"
  },
  {
    "path": "package.json",
    "chars": 1383,
    "preview": "{\n  \"name\": \"page\",\n  \"description\": \"Tiny client-side router\",\n  \"version\": \"1.11.6\",\n  \"license\": \"MIT\",\n  \"repository"
  },
  {
    "path": "page.js",
    "chars": 31759,
    "preview": "(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory()"
  },
  {
    "path": "page.mjs",
    "chars": 31507,
    "preview": "var isarray = Array.isArray || function (arr) {\n  return Object.prototype.toString.call(arr) == '[object Array]';\n};\n\n/*"
  },
  {
    "path": "rollup.config.js",
    "chars": 360,
    "preview": "import commonjs from 'rollup-plugin-commonjs';\nimport nodeResolve from 'rollup-plugin-node-resolve';\n\nexport default {\n "
  },
  {
    "path": "test/demos/back-demo.html",
    "chars": 366,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<title>Blank page</title>\n<body>\n  <ul class=\"links\">\n    <li><a class=\"current-page\" h"
  },
  {
    "path": "test/index.html",
    "chars": 603,
    "preview": "<html>\n  <head>\n    <title>page.js - tests</title>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8"
  },
  {
    "path": "test/mocha.opts",
    "chars": 8,
    "preview": "-R spec\n"
  },
  {
    "path": "test/support/jsdom.js",
    "chars": 1079,
    "preview": "\nfunction setupJsdom(options, next) {\n  options = options || {};\n\n  var jsdom = require('jsdom');\n  var html = '<!doctyp"
  },
  {
    "path": "test/test-page.html",
    "chars": 943,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<title>Blank page</title>\n<body>\n  <ul class=\"links\">\n    <li><a class=\"index\" href=\"./"
  },
  {
    "path": "test/tests.js",
    "chars": 21347,
    "preview": "/* globals before, after, chai, expect, page, describe, it */\n(function() {\n\n  'use strict';\n\n  var isNode = typeof wind"
  }
]

About this extraction

This page contains the full source code of the visionmedia/page.js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 58 files (173.5 KB), approximately 48.1k tokens, and a symbol index with 85 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!