From 43589e583eb137f0cf5d02acb211b9ecd572a42b Mon Sep 17 00:00:00 2001 From: camille Date: Fri, 27 Mar 2026 17:49:26 +0100 Subject: [PATCH] debut des details de la page. Vu que c'est le troisieme (euh quatrieme?) composant, c'etait un peu plus rapide, mais heureusement que claude est la pour repasser derriere mes erreurs prcq en solo je n'y arriverais pas du tout! --- node_modules/.package-lock.json | 90 ++ node_modules/@riotjs/route/LICENSE | 21 + node_modules/@riotjs/route/README.md | 264 +++ node_modules/@riotjs/route/index.d.ts | 26 + node_modules/@riotjs/route/index.js | 1416 ++++++++++++++++ .../@riotjs/route/index.standalone.js | 1039 ++++++++++++ .../@riotjs/route/index.standalone.umd.js | 1058 ++++++++++++ node_modules/@riotjs/route/index.umd.js | 1435 +++++++++++++++++ node_modules/@riotjs/route/package.json | 92 ++ node_modules/@riotjs/util/LICENSE | 21 + node_modules/@riotjs/util/README.md | 19 + node_modules/@riotjs/util/binding-types.cjs | 24 + node_modules/@riotjs/util/binding-types.js | 13 + node_modules/@riotjs/util/checks.cjs | 96 ++ node_modules/@riotjs/util/checks.js | 84 + node_modules/@riotjs/util/constants.cjs | 57 + node_modules/@riotjs/util/constants.js | 28 + node_modules/@riotjs/util/dom.cjs | 97 ++ node_modules/@riotjs/util/dom.js | 86 + .../@riotjs/util/expression-types.cjs | 24 + node_modules/@riotjs/util/expression-types.js | 13 + node_modules/@riotjs/util/functions.cjs | 40 + node_modules/@riotjs/util/functions.js | 33 + node_modules/@riotjs/util/index.cjs | 81 + node_modules/@riotjs/util/index.js | 9 + node_modules/@riotjs/util/misc.cjs | 68 + node_modules/@riotjs/util/misc.js | 62 + node_modules/@riotjs/util/objects.cjs | 95 ++ node_modules/@riotjs/util/objects.js | 85 + node_modules/@riotjs/util/package.json | 104 ++ node_modules/@riotjs/util/strings.cjs | 22 + node_modules/@riotjs/util/strings.js | 17 + node_modules/bianco.attr/LICENSE | 21 + node_modules/bianco.attr/README.md | 160 ++ node_modules/bianco.attr/index.d.ts | 27 + node_modules/bianco.attr/index.js | 145 ++ node_modules/bianco.attr/index.next.js | 131 ++ node_modules/bianco.attr/package.json | 47 + node_modules/bianco.dom-to-array/LICENSE | 21 + node_modules/bianco.dom-to-array/README.md | 62 + node_modules/bianco.dom-to-array/index.d.ts | 4 + node_modules/bianco.dom-to-array/index.js | 27 + .../bianco.dom-to-array/index.next.js | 23 + node_modules/bianco.dom-to-array/package.json | 49 + node_modules/bianco.events/LICENSE | 21 + node_modules/bianco.events/README.md | 83 + node_modules/bianco.events/index.d.ts | 29 + node_modules/bianco.events/index.js | 85 + node_modules/bianco.events/index.next.js | 73 + node_modules/bianco.events/package.json | 50 + node_modules/bianco.query/LICENSE | 21 + node_modules/bianco.query/README.md | 56 + node_modules/bianco.query/index.d.ts | 12 + node_modules/bianco.query/index.js | 22 + node_modules/bianco.query/index.next.js | 14 + node_modules/bianco.query/package.json | 52 + node_modules/cumpa/LICENSE | 21 + node_modules/cumpa/README.md | 74 + node_modules/cumpa/index.cjs | 32 + node_modules/cumpa/index.d.ts | 4 + node_modules/cumpa/index.js | 21 + node_modules/cumpa/package.json | 50 + node_modules/erre/LICENSE | 21 + node_modules/erre/README.md | 353 ++++ node_modules/erre/index.cjs | 243 +++ node_modules/erre/index.d.ts | 26 + node_modules/erre/index.js | 235 +++ node_modules/erre/index.next.js | 147 ++ node_modules/erre/package.json | 55 + node_modules/path-to-regexp/LICENSE | 21 + node_modules/path-to-regexp/Readme.md | 350 ++++ .../path-to-regexp/dist.es2015/index.js | 415 +++++ .../path-to-regexp/dist.es2015/index.js.map | 1 + node_modules/path-to-regexp/dist/index.d.ts | 127 ++ node_modules/path-to-regexp/dist/index.js | 425 +++++ node_modules/path-to-regexp/dist/index.js.map | 1 + node_modules/path-to-regexp/package.json | 63 + node_modules/rawth/LICENSE | 21 + node_modules/rawth/README.md | 100 ++ node_modules/rawth/index.cjs | 776 +++++++++ node_modules/rawth/index.d.ts | 24 + node_modules/rawth/index.js | 758 +++++++++ node_modules/rawth/index.next.js | 164 ++ node_modules/rawth/package.json | 59 + node_modules/ruit/LICENSE | 21 + node_modules/ruit/README.md | 114 ++ node_modules/ruit/index.js | 106 ++ node_modules/ruit/index.next.js | 87 + node_modules/ruit/package.json | 46 + package-lock.json | 91 ++ package.json | 1 + .../src/components/page-details.riot | 52 + 92 files changed, 12959 insertions(+) create mode 100644 node_modules/@riotjs/route/LICENSE create mode 100644 node_modules/@riotjs/route/README.md create mode 100644 node_modules/@riotjs/route/index.d.ts create mode 100644 node_modules/@riotjs/route/index.js create mode 100644 node_modules/@riotjs/route/index.standalone.js create mode 100644 node_modules/@riotjs/route/index.standalone.umd.js create mode 100644 node_modules/@riotjs/route/index.umd.js create mode 100644 node_modules/@riotjs/route/package.json create mode 100644 node_modules/@riotjs/util/LICENSE create mode 100644 node_modules/@riotjs/util/README.md create mode 100644 node_modules/@riotjs/util/binding-types.cjs create mode 100644 node_modules/@riotjs/util/binding-types.js create mode 100644 node_modules/@riotjs/util/checks.cjs create mode 100644 node_modules/@riotjs/util/checks.js create mode 100644 node_modules/@riotjs/util/constants.cjs create mode 100644 node_modules/@riotjs/util/constants.js create mode 100644 node_modules/@riotjs/util/dom.cjs create mode 100644 node_modules/@riotjs/util/dom.js create mode 100644 node_modules/@riotjs/util/expression-types.cjs create mode 100644 node_modules/@riotjs/util/expression-types.js create mode 100644 node_modules/@riotjs/util/functions.cjs create mode 100644 node_modules/@riotjs/util/functions.js create mode 100644 node_modules/@riotjs/util/index.cjs create mode 100644 node_modules/@riotjs/util/index.js create mode 100644 node_modules/@riotjs/util/misc.cjs create mode 100644 node_modules/@riotjs/util/misc.js create mode 100644 node_modules/@riotjs/util/objects.cjs create mode 100644 node_modules/@riotjs/util/objects.js create mode 100644 node_modules/@riotjs/util/package.json create mode 100644 node_modules/@riotjs/util/strings.cjs create mode 100644 node_modules/@riotjs/util/strings.js create mode 100644 node_modules/bianco.attr/LICENSE create mode 100644 node_modules/bianco.attr/README.md create mode 100644 node_modules/bianco.attr/index.d.ts create mode 100644 node_modules/bianco.attr/index.js create mode 100644 node_modules/bianco.attr/index.next.js create mode 100644 node_modules/bianco.attr/package.json create mode 100644 node_modules/bianco.dom-to-array/LICENSE create mode 100644 node_modules/bianco.dom-to-array/README.md create mode 100644 node_modules/bianco.dom-to-array/index.d.ts create mode 100644 node_modules/bianco.dom-to-array/index.js create mode 100644 node_modules/bianco.dom-to-array/index.next.js create mode 100644 node_modules/bianco.dom-to-array/package.json create mode 100644 node_modules/bianco.events/LICENSE create mode 100644 node_modules/bianco.events/README.md create mode 100644 node_modules/bianco.events/index.d.ts create mode 100644 node_modules/bianco.events/index.js create mode 100644 node_modules/bianco.events/index.next.js create mode 100644 node_modules/bianco.events/package.json create mode 100644 node_modules/bianco.query/LICENSE create mode 100644 node_modules/bianco.query/README.md create mode 100644 node_modules/bianco.query/index.d.ts create mode 100644 node_modules/bianco.query/index.js create mode 100644 node_modules/bianco.query/index.next.js create mode 100644 node_modules/bianco.query/package.json create mode 100644 node_modules/cumpa/LICENSE create mode 100644 node_modules/cumpa/README.md create mode 100644 node_modules/cumpa/index.cjs create mode 100644 node_modules/cumpa/index.d.ts create mode 100644 node_modules/cumpa/index.js create mode 100644 node_modules/cumpa/package.json create mode 100644 node_modules/erre/LICENSE create mode 100644 node_modules/erre/README.md create mode 100644 node_modules/erre/index.cjs create mode 100644 node_modules/erre/index.d.ts create mode 100644 node_modules/erre/index.js create mode 100644 node_modules/erre/index.next.js create mode 100644 node_modules/erre/package.json create mode 100644 node_modules/path-to-regexp/LICENSE create mode 100644 node_modules/path-to-regexp/Readme.md create mode 100644 node_modules/path-to-regexp/dist.es2015/index.js create mode 100644 node_modules/path-to-regexp/dist.es2015/index.js.map create mode 100644 node_modules/path-to-regexp/dist/index.d.ts create mode 100644 node_modules/path-to-regexp/dist/index.js create mode 100644 node_modules/path-to-regexp/dist/index.js.map create mode 100644 node_modules/path-to-regexp/package.json create mode 100644 node_modules/rawth/LICENSE create mode 100644 node_modules/rawth/README.md create mode 100644 node_modules/rawth/index.cjs create mode 100644 node_modules/rawth/index.d.ts create mode 100644 node_modules/rawth/index.js create mode 100644 node_modules/rawth/index.next.js create mode 100644 node_modules/rawth/package.json create mode 100644 node_modules/ruit/LICENSE create mode 100644 node_modules/ruit/README.md create mode 100644 node_modules/ruit/index.js create mode 100644 node_modules/ruit/index.next.js create mode 100644 node_modules/ruit/package.json create mode 100644 parcoursup-app/src/components/page-details.riot diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index e3b2bf2..b4eb278 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -3,11 +3,101 @@ "lockfileVersion": 3, "requires": true, "packages": { + "node_modules/@riotjs/route": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@riotjs/route/-/route-10.0.0.tgz", + "integrity": "sha512-NQ9JfIzq/itFthEbCS7GqaVEJ+yjgMqIEZcP2QbiHALcMjyeaR1CacGMMd44e9Gi+Y+N+kOebpTVw2QczTAiqg==", + "license": "MIT", + "dependencies": { + "@riotjs/util": "^10.0.0", + "bianco.attr": "^1.1.1", + "bianco.events": "^1.1.1", + "bianco.query": "^1.1.4", + "cumpa": "^2.0.1", + "rawth": "^3.0.0" + } + }, + "node_modules/@riotjs/util": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/@riotjs/util/-/util-10.1.2.tgz", + "integrity": "sha512-K85suj+5YItWHB5N6LO1uMJNH6ZMBl8FxGH2xDb6dl8V3EBlLuQaPQGVzp3SOKKqeqcxJH3o5UHHIgjc2I8YrA==", + "license": "MIT" + }, + "node_modules/bianco.attr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bianco.attr/-/bianco.attr-1.1.1.tgz", + "integrity": "sha512-fTjfPnnGYiCVbe5UltC/LsDRtJE+MjmadtL749CMIfCwjl18sdbCkaQ7cgtSao6iC9ZJC8Pzw0rjMdIuA6mK1g==", + "license": "MIT", + "dependencies": { + "bianco.dom-to-array": "^1.1.0" + } + }, + "node_modules/bianco.dom-to-array": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bianco.dom-to-array/-/bianco.dom-to-array-1.1.0.tgz", + "integrity": "sha512-IWUgplQRhJSZh+7PgD/my5+X27PXNUFdcHPosOYz39a/iFF8Wl9d0N/mOArdR7Zgr3J0Q9pKVk7nO6W+7XZwBg==", + "license": "MIT" + }, + "node_modules/bianco.events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bianco.events/-/bianco.events-1.1.1.tgz", + "integrity": "sha512-Ja7oY4xThYgsmfS+JltOnzdAvqP90DVXjbXab0lwrygJdCVRoL0Q4SkEKVMnN1VqNfDtxIUKNlubEUVNp00H7A==", + "license": "MIT", + "dependencies": { + "bianco.dom-to-array": "^1.1.0" + } + }, + "node_modules/bianco.query": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bianco.query/-/bianco.query-1.1.4.tgz", + "integrity": "sha512-jUu8l484ckacCBmxN0gYLZ4Ge5aMfReL+aYNiC81s37s8+l0+rn9pnQayEgQtMHGlnL8ejd+x5U2PKpo0rvQzw==", + "license": "MIT", + "dependencies": { + "bianco.dom-to-array": "^1.1.0" + } + }, + "node_modules/cumpa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cumpa/-/cumpa-2.0.1.tgz", + "integrity": "sha512-8oBF1cSWkgYq0ZsLP9iiLLZDicIh1eYM8WLicRhaSLMrdtjEf9A3DgMYMxB/i/xgVUZGxPVD/hrwVzx/pjdmmw==", + "license": "MIT" + }, + "node_modules/erre": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/erre/-/erre-3.0.1.tgz", + "integrity": "sha512-NoexRasUiWU1CcBMh997iybzdKRw4RPhjjiVjPwh1h+aK0PglsR6+7A3osXP5829hXNnarn9Yr1Zi9ThwwV4aA==", + "license": "MIT", + "dependencies": { + "ruit": "^1.0.4" + } + }, "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "license": "BSD-2-Clause" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, + "node_modules/rawth": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rawth/-/rawth-3.0.0.tgz", + "integrity": "sha512-712WxtKAVEhtFm/4keDUefyjxAZR/jKrFR8ownCdjnFUxJstyigv3LBo3Shtd5VPC/p6r5gd6jY0bnAfPSol9Q==", + "license": "MIT", + "dependencies": { + "erre": "^3.0.1", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/ruit": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ruit/-/ruit-1.0.4.tgz", + "integrity": "sha512-eiHVb18DQ24Of/fdJmZCysw6X21IIyed5c87eAW95KQY5TvTfh6SR9pCkAowciyvhW1Bhm3RXuRX6eILKl+49w==", + "license": "MIT" } } } diff --git a/node_modules/@riotjs/route/LICENSE b/node_modules/@riotjs/route/LICENSE new file mode 100644 index 0000000..74e6791 --- /dev/null +++ b/node_modules/@riotjs/route/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Gianluca Guarini + +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. diff --git a/node_modules/@riotjs/route/README.md b/node_modules/@riotjs/route/README.md new file mode 100644 index 0000000..69b0931 --- /dev/null +++ b/node_modules/@riotjs/route/README.md @@ -0,0 +1,264 @@ +# Riot Router + +[![Route logo](https://raw.githubusercontent.com/riot/branding/main/route/route-horizontal.svg)](https://github.com/riot/route/) + +[![Build Status][ci-image]][ci-url] [![Code Quality][qlty-image]][qlty-url] [![NPM version][npm-version-image]][npm-url] [![NPM downloads][npm-downloads-image]][npm-url] [![MIT License][license-image]][license-url] [![Coverage Status][coverage-image]][coverage-url] + +> Simple isomorphic router + +The Riot.js Router is the minimal router implementation with such technologies: + +- compatible with the DOM pushState and history API +- isomorphic functional API +- [erre.js streams](https://github.com/GianlucaGuarini/erre) and javascript async generators +- [rawth.js](https://github.com/GianlucaGuarini/rawth) urls parsing + +It doesn't need Riot.js to work and can be used as standalone module. + +**For Riot.js 3 and the older route version please check the [v3 branch](https://github.com/riot/route/tree/v3)** + +## Table of Contents + +- [Install](#install) +- [Documentation](#documentation) +- [Demos](https://github.com/riot/examples) + +## Install + +We have 2 editions: + +| edition | file | +| :------------------------ | :------------------------ | +| **ESM Module** | `index.js` | +| **UMD Version** | `index.umd.js` | +| **Standalone ESM Module** | `index.standalone.js` | +| **Standalone UMD Module** | `index.standalone.umd.js` | + +### Script injection + +```html + +``` + +_Note_: change the part `x.x.x` to the version numbers what you want to use: ex. `4.5.0` or `4.7.0`. + +### ESM module + +```js +import { route } from 'https://unpkg.com/@riotjs/route/index.js' +``` + +### npm + +```bash +npm i -S @riotjs/route +``` + +### Download by yourself + +- [Standalone](https://unpkg.com/@riotjs/route/route.js) +- [ESM](https://unpkg.com/@riotjs/route/route.esm.js) + +## Documentation + +### With Riot.js + +You can import the `` and `` components in your application and use them as it follows: + +```html + + + + + + + Home page + About + Hello dear { route.params.person } + + + + +``` + +You can also use the `riot.register` method to register them globally + +```js +import { Route, Router } from '@riotjs/route' +import { register } from 'riot' + +// now the Router and Route components are globally available +register('router', Router) +register('route', Route) +``` + +#### Router + +The `` component should wrap your application markup and will detect automatically all the clicks on links that should trigger a route event. + +```html + + + Link + + +Link +``` + +You can also specify the base of your application via component attributes: + +```html + + + Link + +``` + +The router component has also an `onStarted` callback that will be called asynchronously after the first route event will be called + +```html + +``` + +#### Route + +The `` component provides the `route` property to its children (it's simply a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object) allowing you to detect the url params and queries. + +```html + {JSON.stringify(route.params)} + + + + + {route.searchParams.get('q')} + +``` + +Each `` component has its own lifecycle attributes in order to let you know when it gets mounted or unmounted. + +```riot + + + + + +``` + +### Standalone + +This module was not only designed to be used with Riot.js but also as standalone module. +Without importing the Riot.js components in your application you can use the core methods exported to build and customize your own router compatible with any kind of frontend setup. + +Depending on your project setup you might import it as follows: + +```js +// in a Riot.js application +import { route } from '@riotjs/route' + +// in a standalone context +import { route } from '@riotjs/route/standalone' +``` + +#### Fundamentals + +This module works on node and on any modern browser, it exports the `router` and `router` property exposed by [rawth](https://github.com/GianlucaGuarini/rawth) + +```js +import { route, router, setBase } from '@riotjs/route' + +// required to set base first +setBase('/') + +// create a route stream +const aboutStream = route('/about') + +aboutStream.on.value((url) => { + console.log(url) // URL object +}) + +aboutStream.on.value(() => { + console.log('just log that the about route was triggered') +}) + +// triggered on each route event +router.on.value((path) => { + // path is always a string in this function + console.log(path) +}) + +// trigger a route change manually +router.push('/about') + +// end the stream +aboutStream.end() +``` + +#### Base path + +Before using the router in your browser you will need to set your application base path. +This setting can be configured simply via `setBase` method: + +```js +import { setBase } from '@riotjs/route' + +// in case you want to use the HTML5 history navigation +setBase(`/`) + +// in case you use the hash navigation +setBase(`#`) +``` + +Setting the base path of your application route is mandatory and is the first you probably are going to do before creating your route listeners. + +#### DOM binding + +The example above is not really practical in case you are working in a browser environment. In that case you might want to bind your router to the DOM listening all the click events that might trigger a route change event. +Window history `popstate` events should be also connected to the router. +With the `initDomListeners` method you can automatically achieve all the features above: + +```js +import { initDomListeners } from '@riotjs/route' + +const unsubscribe = initDomListeners() +// the router is connected to the page DOM + +// ...tear down and disconnect the router from the DOM +unsubscribe() +``` + +The `initDomListeners` will intercept any link click on your application. However it can also receive a HTMLElement or a list of HTMLElements as argument to scope the click listener only to a specific DOM region of your application + +```js +import { initDomListeners } from '@riotjs/route' + +initDomListeners(document.querySelector('.main-navigation')) +``` + +[ci-image]: https://img.shields.io/github/actions/workflow/status/riot/route/test.yml?style=flat-square +[ci-url]: https://github.com/riot/route/actions +[license-image]: http://img.shields.io/badge/license-MIT-000000.svg?style=flat-square +[license-url]: LICENSE.txt +[npm-version-image]: http://img.shields.io/npm/v/@riotjs/route.svg?style=flat-square +[npm-downloads-image]: http://img.shields.io/npm/dm/@riotjs/route.svg?style=flat-square +[npm-url]: https://npmjs.org/package/@riotjs/route +[coverage-image]: https://qlty.sh/gh/riot/projects/route/coverage.svg +[coverage-url]: https://qlty.sh/gh/riot/projects/route +[qlty-image]: https://qlty.sh/gh/riot/projects/route/maintainability.svg +[qlty-url]: https://qlty.sh/gh/riot/projects/route diff --git a/node_modules/@riotjs/route/index.d.ts b/node_modules/@riotjs/route/index.d.ts new file mode 100644 index 0000000..a39ba31 --- /dev/null +++ b/node_modules/@riotjs/route/index.d.ts @@ -0,0 +1,26 @@ +import { RiotComponentWrapper, RiotComponent } from 'riot' +import { URLWithParams } from 'rawth' + +export * from 'rawth' + +export declare const Route: RiotComponentWrapper< + RiotComponent<{ + path: string + 'on-before-mount'?: (path: URLWithParams) => void + 'on-mounted'?: (path: URLWithParams) => void + 'on-before-unmount'?: (path: URLWithParams) => void + 'on-unmounted'?: (path: URLWithParams) => void + }> +> + +export declare const Router: RiotComponentWrapper< + RiotComponent<{ + base?: string + 'initial-route'?: string + 'on-started'?: (route: string) => void + }> +> + +export declare function getCurrentRoute(): string +export declare function initDomListeners(): void +export declare function setBase(base: string): void diff --git a/node_modules/@riotjs/route/index.js b/node_modules/@riotjs/route/index.js new file mode 100644 index 0000000..399c092 --- /dev/null +++ b/node_modules/@riotjs/route/index.js @@ -0,0 +1,1416 @@ +import { __, pure } from 'riot'; + +/** + * Tokenize input string. + */ +function lexer(str) { + var tokens = []; + var i = 0; + while (i < str.length) { + var char = str[i]; + if (char === "*" || char === "+" || char === "?") { + tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); + continue; + } + if (char === "\\") { + tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); + continue; + } + if (char === "{") { + tokens.push({ type: "OPEN", index: i, value: str[i++] }); + continue; + } + if (char === "}") { + tokens.push({ type: "CLOSE", index: i, value: str[i++] }); + continue; + } + if (char === ":") { + var name = ""; + var j = i + 1; + while (j < str.length) { + var code = str.charCodeAt(j); + if ( + // `0-9` + (code >= 48 && code <= 57) || + // `A-Z` + (code >= 65 && code <= 90) || + // `a-z` + (code >= 97 && code <= 122) || + // `_` + code === 95) { + name += str[j++]; + continue; + } + break; + } + if (!name) + throw new TypeError("Missing parameter name at ".concat(i)); + tokens.push({ type: "NAME", index: i, value: name }); + i = j; + continue; + } + if (char === "(") { + var count = 1; + var pattern = ""; + var j = i + 1; + if (str[j] === "?") { + throw new TypeError("Pattern cannot start with \"?\" at ".concat(j)); + } + while (j < str.length) { + if (str[j] === "\\") { + pattern += str[j++] + str[j++]; + continue; + } + if (str[j] === ")") { + count--; + if (count === 0) { + j++; + break; + } + } + else if (str[j] === "(") { + count++; + if (str[j + 1] !== "?") { + throw new TypeError("Capturing groups are not allowed at ".concat(j)); + } + } + pattern += str[j++]; + } + if (count) + throw new TypeError("Unbalanced pattern at ".concat(i)); + if (!pattern) + throw new TypeError("Missing pattern at ".concat(i)); + tokens.push({ type: "PATTERN", index: i, value: pattern }); + i = j; + continue; + } + tokens.push({ type: "CHAR", index: i, value: str[i++] }); + } + tokens.push({ type: "END", index: i, value: "" }); + return tokens; +} +/** + * Parse a string for the raw tokens. + */ +function parse(str, options) { + if (options === void 0) { options = {}; } + var tokens = lexer(str); + var _a = options.prefixes, prefixes = _a === void 0 ? "./" : _a; + var defaultPattern = "[^".concat(escapeString(options.delimiter || "/#?"), "]+?"); + var result = []; + var key = 0; + var i = 0; + var path = ""; + var tryConsume = function (type) { + if (i < tokens.length && tokens[i].type === type) + return tokens[i++].value; + }; + var mustConsume = function (type) { + var value = tryConsume(type); + if (value !== undefined) + return value; + var _a = tokens[i], nextType = _a.type, index = _a.index; + throw new TypeError("Unexpected ".concat(nextType, " at ").concat(index, ", expected ").concat(type)); + }; + var consumeText = function () { + var result = ""; + var value; + while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { + result += value; + } + return result; + }; + while (i < tokens.length) { + var char = tryConsume("CHAR"); + var name = tryConsume("NAME"); + var pattern = tryConsume("PATTERN"); + if (name || pattern) { + var prefix = char || ""; + if (prefixes.indexOf(prefix) === -1) { + path += prefix; + prefix = ""; + } + if (path) { + result.push(path); + path = ""; + } + result.push({ + name: name || key++, + prefix: prefix, + suffix: "", + pattern: pattern || defaultPattern, + modifier: tryConsume("MODIFIER") || "", + }); + continue; + } + var value = char || tryConsume("ESCAPED_CHAR"); + if (value) { + path += value; + continue; + } + if (path) { + result.push(path); + path = ""; + } + var open = tryConsume("OPEN"); + if (open) { + var prefix = consumeText(); + var name_1 = tryConsume("NAME") || ""; + var pattern_1 = tryConsume("PATTERN") || ""; + var suffix = consumeText(); + mustConsume("CLOSE"); + result.push({ + name: name_1 || (pattern_1 ? key++ : ""), + pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1, + prefix: prefix, + suffix: suffix, + modifier: tryConsume("MODIFIER") || "", + }); + continue; + } + mustConsume("END"); + } + return result; +} +/** + * Compile a string to a template function for the path. + */ +function compile(str, options) { + return tokensToFunction(parse(str, options), options); +} +/** + * Expose a method for transforming tokens into the path function. + */ +function tokensToFunction(tokens, options) { + if (options === void 0) { options = {}; } + var reFlags = flags(options); + var _a = options.encode, encode = _a === void 0 ? function (x) { return x; } : _a, _b = options.validate, validate = _b === void 0 ? true : _b; + // Compile all the tokens into regexps. + var matches = tokens.map(function (token) { + if (typeof token === "object") { + return new RegExp("^(?:".concat(token.pattern, ")$"), reFlags); + } + }); + return function (data) { + var path = ""; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (typeof token === "string") { + path += token; + continue; + } + var value = data ? data[token.name] : undefined; + var optional = token.modifier === "?" || token.modifier === "*"; + var repeat = token.modifier === "*" || token.modifier === "+"; + if (Array.isArray(value)) { + if (!repeat) { + throw new TypeError("Expected \"".concat(token.name, "\" to not repeat, but got an array")); + } + if (value.length === 0) { + if (optional) + continue; + throw new TypeError("Expected \"".concat(token.name, "\" to not be empty")); + } + for (var j = 0; j < value.length; j++) { + var segment = encode(value[j], token); + if (validate && !matches[i].test(segment)) { + throw new TypeError("Expected all \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\"")); + } + path += token.prefix + segment + token.suffix; + } + continue; + } + if (typeof value === "string" || typeof value === "number") { + var segment = encode(String(value), token); + if (validate && !matches[i].test(segment)) { + throw new TypeError("Expected \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\"")); + } + path += token.prefix + segment + token.suffix; + continue; + } + if (optional) + continue; + var typeOfMessage = repeat ? "an array" : "a string"; + throw new TypeError("Expected \"".concat(token.name, "\" to be ").concat(typeOfMessage)); + } + return path; + }; +} +/** + * Escape a regular expression string. + */ +function escapeString(str) { + return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); +} +/** + * Get the flags for a regexp from the options. + */ +function flags(options) { + return options && options.sensitive ? "" : "i"; +} +/** + * Pull out keys from a regexp. + */ +function regexpToRegexp(path, keys) { + if (!keys) + return path; + var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; + var index = 0; + var execResult = groupsRegex.exec(path.source); + while (execResult) { + keys.push({ + // Use parenthesized substring match if available, index otherwise + name: execResult[1] || index++, + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }); + execResult = groupsRegex.exec(path.source); + } + return path; +} +/** + * Transform an array into a regexp. + */ +function arrayToRegexp(paths, keys, options) { + var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source; }); + return new RegExp("(?:".concat(parts.join("|"), ")"), flags(options)); +} +/** + * Create a path regexp from string input. + */ +function stringToRegexp(path, keys, options) { + return tokensToRegexp(parse(path, options), keys, options); +} +/** + * Expose a function for taking tokens and returning a RegExp. + */ +function tokensToRegexp(tokens, keys, options) { + if (options === void 0) { options = {}; } + var _a = options.strict, strict = _a === void 0 ? false : _a, _b = options.start, start = _b === void 0 ? true : _b, _c = options.end, end = _c === void 0 ? true : _c, _d = options.encode, encode = _d === void 0 ? function (x) { return x; } : _d, _e = options.delimiter, delimiter = _e === void 0 ? "/#?" : _e, _f = options.endsWith, endsWith = _f === void 0 ? "" : _f; + var endsWithRe = "[".concat(escapeString(endsWith), "]|$"); + var delimiterRe = "[".concat(escapeString(delimiter), "]"); + var route = start ? "^" : ""; + // Iterate over the tokens and create our regexp string. + for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { + var token = tokens_1[_i]; + if (typeof token === "string") { + route += escapeString(encode(token)); + } + else { + var prefix = escapeString(encode(token.prefix)); + var suffix = escapeString(encode(token.suffix)); + if (token.pattern) { + if (keys) + keys.push(token); + if (prefix || suffix) { + if (token.modifier === "+" || token.modifier === "*") { + var mod = token.modifier === "*" ? "?" : ""; + route += "(?:".concat(prefix, "((?:").concat(token.pattern, ")(?:").concat(suffix).concat(prefix, "(?:").concat(token.pattern, "))*)").concat(suffix, ")").concat(mod); + } + else { + route += "(?:".concat(prefix, "(").concat(token.pattern, ")").concat(suffix, ")").concat(token.modifier); + } + } + else { + if (token.modifier === "+" || token.modifier === "*") { + route += "((?:".concat(token.pattern, ")").concat(token.modifier, ")"); + } + else { + route += "(".concat(token.pattern, ")").concat(token.modifier); + } + } + } + else { + route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier); + } + } + } + if (end) { + if (!strict) + route += "".concat(delimiterRe, "?"); + route += !options.endsWith ? "$" : "(?=".concat(endsWithRe, ")"); + } + else { + var endToken = tokens[tokens.length - 1]; + var isEndDelimited = typeof endToken === "string" + ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 + : endToken === undefined; + if (!strict) { + route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?"); + } + if (!isEndDelimited) { + route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")"); + } + } + 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 }]`. + */ +function pathToRegexp(path, keys, options) { + if (path instanceof RegExp) + return regexpToRegexp(path, keys); + if (Array.isArray(path)) + return arrayToRegexp(path, keys, options); + return stringToRegexp(path, keys, options); +} + +/** + * Cancel token + * @private + * @type { Symbol } + */ +const CANCEL = Symbol(); + +/** + * Helper that can be returned by ruit function to cancel the tasks chain + * @returns { Symbol } internal private constant + * @example + * + * ruit( + * 100, + * num => Math.random() * num + * num => num > 50 ? ruit.cancel() : num + * num => num - 2 + * ).then(result => { + * console.log(result) // here we will get only number lower than 50 + * }) + * + */ +ruit.cancel = () => CANCEL; + +/** + * The same as ruit() but with the arguments inverted from right to left + * @param { * } tasks - list of tasks to process sequentially + * @returns { Promise } a promise containing the result of the whole chain + * @example + * + * const curry = f => a => b => f(a, b) + * const add = (a, b) => a + b + * + * const addOne = curry(add)(1) + * + * const squareAsync = (num) => { + * return new Promise(r => { + * setTimeout(r, 500, num * 2) + * }) + * } + * + * // a -> a + a -> a * 2 + * // basically from right to left: 1 => 1 + 1 => 2 * 2 + * ruit.compose(squareAsync, addOne, 1).then(result => console.log(result)) // 4 + */ +ruit.compose = (...tasks) => ruit(...tasks.reverse()); + +/** + * Serialize a list of sync and async tasks from left to right + * @param { * } tasks - list of tasks to process sequentially + * @returns { Promise } a promise containing the result of the whole chain + * @example + * + * const curry = f => a => b => f(a, b) + * const add = (a, b) => a + b + * + * const addOne = curry(add)(1) + * + * const squareAsync = (num) => { + * return new Promise(r => { + * setTimeout(r, 500, num * 2) + * }) + * } + * + * // a -> a + a -> a * 2 + * // basically from left to right: 1 => 1 + 1 => 2 * 2 + * ruit(1, addOne, squareAsync).then(result => console.log(result)) // 4 + */ +function ruit(...tasks) { + return new Promise((resolve, reject) => { + return (function run(queue, result) { + if (!queue.length) return resolve(result) + + const [task, ...rest] = queue; + const value = typeof task === 'function' ? task(result) : task; + const done = v => run(rest, v); + + // check against nil values + if (value != null) { + if (value === CANCEL) return + if (value.then) return value.then(done, reject) + } + + return Promise.resolve(done(value)) + })(tasks) + }) +} + +// Store the erre the API methods to handle the plugins installation +const API_METHODS = new Set(); +const UNSUBSCRIBE_SYMBOL = Symbol(); +const UNSUBSCRIBE_METHOD = 'off'; +const CANCEL_METHOD = 'cancel'; + +/** + * Factory function to create the stream generator + * @private + * @param {Set} modifiers - stream input modifiers + * @returns {Generator} the stream generator + */ +function createStream(modifiers) { + const stream = (function *stream() { + while (true) { + // get the initial stream value + const input = yield; + + // run the input sequence + yield ruit(input, ...modifiers); + } + })(); + + // start the stream + stream.next(); + + return stream +} + +/** + * Dispatch a value to several listeners + * @private + * @param {Set} callbacks - callbacks collection + * @param {*} value - anything + * @returns {Set} the callbacks received + */ +function dispatch(callbacks, value) { + callbacks.forEach(f => { + // unsubscribe the callback if erre.unsubscribe() will be returned + if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f); + }); + + return callbacks +} + +/** + * Throw a panic error + * @param {string} message - error message + * @returns {Error} an error object + */ +function panic$1(message) { + throw new Error(message) +} + +/** + * Install an erre plugin adding it to the API + * @param {string} name - plugin name + * @param {Function} fn - new erre API method + * @returns {Function} return the erre function + */ +erre.install = function(name, fn) { + if (!name || typeof name !== 'string') + panic$1('Please provide a name (as string) for your erre plugin'); + if (!fn || typeof fn !== 'function') + panic$1('Please provide a function for your erre plugin'); + + if (API_METHODS.has(name)) { + panic$1(`The ${name} is already part of the erre API, please provide a different name`); + } else { + erre[name] = fn; + API_METHODS.add(name); + } + + return erre +}; + +// alias for ruit canel to stop a stream chain +erre.install(CANCEL_METHOD, ruit.cancel); + +// unsubscribe helper +erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL); + +/** + * Stream constuction function + * @param {...Function} fns - stream modifiers + * @returns {Object} erre instance + */ +function erre(...fns) { + const + [success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)], + generator = createStream(modifiers), + stream = Object.create(generator), + addToCollection = (collection) => (fn) => collection.add(fn) && stream, + deleteFromCollection = (collection) => (fn) => collection.delete(fn) ? stream + : panic$1('Couldn\'t remove handler passed by reference'); + + return Object.assign(stream, { + on: Object.freeze({ + value: addToCollection(success), + error: addToCollection(error), + end: addToCollection(end) + }), + off: Object.freeze({ + value: deleteFromCollection(success), + error: deleteFromCollection(error), + end: deleteFromCollection(end) + }), + connect: addToCollection(modifiers), + push(input) { + const { value, done } = stream.next(input); + + // dispatch the stream events + if (!done) { + value.then( + res => dispatch(success, res), + err => dispatch(error, err) + ); + } + + return stream + }, + end() { + // kill the stream + generator.return(); + // dispatch the end event + dispatch(end) + // clean up all the collections + ;[success, error, end, modifiers].forEach(el => el.clear()); + + return stream + }, + fork() { + return erre(...modifiers) + }, + next(input) { + // get the input and run eventually the promise + const result = generator.next(input); + + // pause to the next iteration + generator.next(); + + return result + } + }) +} + +const isString = str => typeof str === 'string'; +const parseURL = (...args) => new URL(...args); + +/** + * Replace the base path from a path + * @param {string} path - router path string + * @returns {string} path cleaned up without the base + */ +const replaceBase = path => path.replace(defaults.base, ''); + +/** + * Try to match the current path or skip it + * @param {RegExp} pathRegExp - target path transformed by pathToRegexp + * @returns {string|Symbol} if the path match we return it otherwise we cancel the stream + */ +const matchOrSkip = pathRegExp => path => match(path, pathRegExp) ? path : erre.cancel(); + +/** + * Combine 2 streams connecting the events of dispatcherStream to the receiverStream + * @param {Stream} dispatcherStream - main stream dispatching events + * @param {Stream} receiverStream - sub stream receiving events from the dispatcher + * @returns {Stream} receiverStream + */ +const joinStreams = (dispatcherStream, receiverStream) => { + dispatcherStream.on.value(receiverStream.push); + + receiverStream.on.end(() => { + dispatcherStream.off.value(receiverStream.push); + }); + + return receiverStream +}; + +/** + * Error handling function + * @param {Error} error - error to catch + * @returns {void} + */ +/* c8 ignore start */ +const panic$2 = error => { + if (defaults.silentErrors) return + + throw new Error(error) +}; +/* c8 ignore stop */ + +// make sure that the router will always receive strings params +const filterStrings = str => isString(str) ? str : erre.cancel(); + +// create the streaming router +const router = erre(filterStrings).on.error(panic$2); // cast the values of this stream always to string + +/** + * Merge the user options with the defaults + * @param {Object} options - custom user options + * @returns {Object} options object merged with defaults + */ +const mergeOptions = options => ({...defaults, ...options}); + +/* @type {object} general configuration object */ +const defaults = { + base: 'https://localhost', + silentErrors: false, + // pathToRegexp options + sensitive: false, + strict: false, + end: true, + start: true, + delimiter: '/#?', + encode: undefined, + endsWith: undefined, + prefixes: './' +}; + +/** + * Configure the router options overriding the defaults + * @param {Object} options - custom user options to override + * @returns {Object} new defaults + */ +const configure = (options) => { + Object.entries(options).forEach(([key, value]) => { + if (Object.hasOwn(defaults, key)) defaults[key] = value; + }); + + return defaults +}; + + + +/* {@link https://github.com/pillarjs/path-to-regexp#usage} */ +const toRegexp = (path, keys, options) => pathToRegexp(path, keys, mergeOptions(options)); + +/** + * Convert a router entry to a real path computing the url parameters + * @param {string} path - router path string + * @param {Object} params - named matched parameters + * @param {Object} options - pathToRegexp options object + * @returns {string} computed url string + */ +const toPath = (path, params, options) => compile(path, mergeOptions(options))(params); + +/** + * Parse a string path generating an object containing + * @param {string} path - target path + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @param {Object} options - object containing the base path + * @returns {URL} url object enhanced with the `match` attribute + */ +const toURL = (path, pathRegExp, options = {}) => { + const {base} = mergeOptions(options); + const [, ...params] = pathRegExp.exec(path); + const url = parseURL(path, base); + + // extend the url object adding the matched params + url.params = params.reduce((acc, param, index) => { + const key = options.keys && options.keys[index]; + if (key) acc[key.name] = param ? decodeURIComponent(param) : param; + return acc + }, {}); + + return url +}; + +/** + * Return true if a path will be matched + * @param {string} path - target path + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @returns {boolean} true if the path matches the regexp + */ +const match = (path, pathRegExp) => pathRegExp.test(path); + +/** + * Factory function to create an sequence of functions to pass to erre.js + * This function will be used in the erre stream + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @param {Object} options - pathToRegexp options object + * @returns {Array} a functions array that will be used as stream pipe for erre.js + */ +const createURLStreamPipe = (pathRegExp, options) => [ + decodeURI, + replaceBase, + matchOrSkip(pathRegExp), + path => toURL(path, pathRegExp, options) +]; + +/** + * Create a fork of the main router stream + * @param {string} path - route to match + * @param {Object} options - pathToRegexp options object + * @returns {Stream} new route stream + */ +function createRoute(path, options) { + const keys = []; + const pathRegExp = pathToRegexp(path, keys, options); + const URLStream = erre(...createURLStreamPipe(pathRegExp, { + ...options, + keys + })); + + return joinStreams(router, URLStream).on.error(panic$2) +} + +const WINDOW_EVENTS = 'popstate'; +const CLICK_EVENT = 'click'; +const DOWNLOAD_LINK_ATTRIBUTE = 'download'; +const HREF_LINK_ATTRIBUTE = 'href'; +const TARGET_SELF_LINK_ATTRIBUTE = '_self'; +const LINK_TAG_NAME = 'A'; +const HASH = '#'; +const SLASH = '/'; +const PATH_ATTRIBUTE = 'path'; +const RE_ORIGIN = /^.+?\/\/+[^/]+/; + +/** + * Converts any DOM node/s to a loopable array + * @param { HTMLElement|NodeList } els - single html element or a node list + * @returns { Array } always a loopable object + */ +function domToArray(els) { + // can this object be already looped? + if (!Array.isArray(els)) { + // is it a node list? + if ( + /^\[object (HTMLCollection|NodeList|Object)\]$/ + .test(Object.prototype.toString.call(els)) + && typeof els.length === 'number' + ) + return Array.from(els) + else + // if it's a single node + // it will be returned as "array" with one single entry + return [els] + } + // this object could be looped out of the box + return els +} + +/** + * Simple helper to find DOM nodes returning them as array like loopable object + * @param { string|DOMNodeList } selector - either the query or the DOM nodes to arraify + * @param { HTMLElement } scope - context defining where the query will search for the DOM nodes + * @returns { Array } DOM nodes found as array + */ +function $(selector, scope) { + return domToArray(typeof selector === 'string' ? + (document).querySelectorAll(selector) : + selector + ) +} + +const getCurrentRoute = ((currentRoute) => { + // listen the route changes events to store the current route + router.on.value((r) => (currentRoute = r)); + + return () => { + return currentRoute + } +})(null); + +/** + * Normalize the return values, in case of a single value we avoid to return an array + * @param { Array } values - list of values we want to return + * @returns { Array|string|boolean } either the whole list of values or the single one found + * @private + */ +const normalize = values => values.length === 1 ? values[0] : values; + +/** + * Parse all the nodes received to get/remove/check their attributes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes + * @param { string } method - method that will be used to parse the attributes + * @returns { Array|string } result of the parsing in a list or a single value + * @private + */ +function parseNodes(els, name, method) { + const names = typeof name === 'string' ? [name] : name; + return normalize(domToArray(els).map(el => { + return normalize(names.map(n => el[method](n))) + })) +} + +/** + * Get any attribute from a single or a list of DOM nodes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes to get + * @returns { Array|string } list of the attributes found + * + * @example + * + * import { get } from 'bianco.attr' + * + * const img = document.createElement('img') + * + * get(img, 'width') // => '200' + * + * // or also + * get(img, ['width', 'height']) // => ['200', '300'] + * + * // or also + * get([img1, img2], ['width', 'height']) // => [['200', '300'], ['500', '200']] + */ +function get(els, name) { + return parseNodes(els, name, 'getAttribute') +} + +/** + * Set any attribute on a single or a list of DOM nodes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes to detect + * @returns { boolean|Array } true or false or an array of boolean values + * @example + * + * import { has } from 'bianco.attr' + * + * has(img, 'width') // false + * + * // or also + * has(img, ['width', 'height']) // => [false, false] + * + * // or also + * has([img1, img2], ['width', 'height']) // => [[false, false], [false, false]] + */ +function has(els, name) { + return parseNodes(els, name, 'hasAttribute') +} + +/** + * Convert a string from camel case to dash-case + * @param {string} string - probably a component tag name + * @returns {string} component name normalized + */ + +/** + * Convert a string containing dashes to camel case + * @param {string} string - input string + * @returns {string} my-string -> myString + */ +function dashToCamelCase(string) { + return string.replace(/-(\w)/g, (_, c) => c.toUpperCase()) +} + +/** + * Check if a value is null or undefined + * @param {*} value - anything + * @returns {boolean} true only for the 'undefined' and 'null' types + */ +function isNil(value) { + return value === null || value === undefined +} + +const getGlobal = () => getWindow() || global; +const getWindow = () => (typeof window === 'undefined' ? null : window); +const getDocument = () => + typeof document === 'undefined' ? null : document; +const getHistory = () => + typeof history === 'undefined' ? null : history; +const getLocation = () => { + const win = getWindow(); + return win ? win.location : {} +}; + +const defer = (() => { + const globalScope = getGlobal(); + + return globalScope.requestAnimationFrame || globalScope.setTimeout +})(); + +const cancelDefer = (() => { + const globalScope = getGlobal(); + + return globalScope.cancelAnimationFrame || globalScope.clearTimeout +})(); + +const getAttribute = (attributes, name, context) => { + if (!attributes) return null + + const normalizedAttributes = attributes.flatMap((attr) => + isNil(attr.name) + ? // add support for spread attributes https://github.com/riot/route/issues/178 + Object.entries(attr.evaluate(context)).map(([key, value]) => ({ + // evaluate each value of the spread attribute and store it into the array + name: key, + // create a nested evaluate function pointing to the original value of the spread object + evaluate: () => value, + })) + : attr, + ); + + return normalizedAttributes.find((a) => dashToCamelCase(a.name) === name) +}; + +const createDefaultSlot = (attributes = []) => { + const { template, bindingTypes, expressionTypes } = __.DOMBindings; + + return template(null, [ + { + type: bindingTypes.SLOT, + name: 'default', + attributes: attributes.map((attr) => ({ + ...attr, + type: expressionTypes.ATTRIBUTE, + })), + }, + ]) +}; + +// True if the selector string is valid +const isValidQuerySelectorString = (selector) => + /^([a-zA-Z0-9-_*#.:[\]\s>+~()='"]|\\.)+$/.test(selector); + +/** + * Similar to compose but performs from left-to-right function composition.
+ * {@link https://30secondsofcode.org/function#composeright see also} + * @param {...[function]} fns) - list of unary function + * @returns {*} result of the computation + */ + +/** + * Performs right-to-left function composition.
+ * Use Array.prototype.reduce() to perform right-to-left function composition.
+ * The last (rightmost) function can accept one or more arguments; the remaining functions must be unary.
+ * {@link https://30secondsofcode.org/function#compose original source code} + * @param {...[function]} fns) - list of unary function + * @returns {*} result of the computation + */ +function compose(...fns) { + return fns.reduce((f, g) => (...args) => f(g(...args))) +} + +const getInitialRouteValue = (pathToRegexp, path, options) => { + const route = compose( + ...createURLStreamPipe(pathToRegexp, options).reverse(), + )(path); + + return route.params ? route : null +}; + +const clearDOMBetweenNodes = (first, last, includeBoundaries) => { + const clear = (node) => { + if (!node || (node === last && !includeBoundaries)) return + const { nextSibling } = node; + node.remove(); + clear(nextSibling); + }; + + clear(includeBoundaries ? first : first.nextSibling); +}; + +const routeHoc$1 = ({ slots, attributes }) => { + const placeholders = { + before: document.createTextNode(''), + after: document.createTextNode(''), + }; + + return { + mount(el, context) { + // create the component state + const currentRoute = getCurrentRoute(); + const path = + getAttribute(attributes, PATH_ATTRIBUTE, context)?.evaluate(context) || + get(el, PATH_ATTRIBUTE); + const pathToRegexp = toRegexp(path, []); + const state = { + pathToRegexp, + route: + currentRoute && match(currentRoute, pathToRegexp) + ? getInitialRouteValue(pathToRegexp, currentRoute, {}) + : null, + }; + this.el = el; + this.slot = createDefaultSlot([ + { + isBoolean: false, + name: 'route', + evaluate: () => this.state.route, + }, + ]); + this.context = context; + this.state = state; + // set the route listeners + this.boundOnBeforeRoute = this.onBeforeRoute.bind(this); + this.boundOnRoute = this.onRoute.bind(this); + router.on.value(this.boundOnBeforeRoute); + this.stream = createRoute(path).on.value(this.boundOnRoute); + // update the DOM + el.replaceWith(placeholders.before); + placeholders.before.parentNode.insertBefore( + placeholders.after, + placeholders.before.nextSibling, + ); + if (state.route) this.mountSlot(); + }, + update(context) { + this.context = context; + if (this.state.route) this.slot.update({}, context); + }, + mountSlot() { + const { route } = this.state; + // insert the route root element after the before placeholder + placeholders.before.parentNode.insertBefore( + this.el, + placeholders.before.nextSibling, + ); + this.callLifecycleProperty('onBeforeMount', route); + this.slot.mount( + this.el, + { + slots, + }, + this.context, + ); + this.callLifecycleProperty('onMounted', route); + }, + clearDOM(includeBoundaries) { + // remove all the DOM nodes between the placeholders + clearDOMBetweenNodes( + placeholders.before, + placeholders.after, + includeBoundaries, + ); + }, + unmount() { + router.off.value(this.boundOnBeforeRoute); + this.slot.unmount({}, this.context, true); + this.clearDOM(true); + this.stream.end(); + }, + onBeforeRoute(path) { + const { route } = this.state; + // this component was not mounted or the current path matches + // we don't need to unmount this component + if (!route || match(path, this.state.pathToRegexp)) return + + this.callLifecycleProperty('onBeforeUnmount', route); + this.slot.unmount({}, this.context, true); + this.clearDOM(false); + this.state.route = null; + this.callLifecycleProperty('onUnmounted', route); + }, + onRoute(route) { + const prevRoute = this.state.route; + this.state.route = route; + + // if this route component was already mounted we need to update it + if (prevRoute) { + this.callLifecycleProperty('onBeforeUpdate', route); + this.slot.update({}, this.context); + this.callLifecycleProperty('onUpdated', route); + } + // this route component was never mounted, so we need to create its DOM + else this.mountSlot(); + + // emulate the default browser anchor links behaviour + if (route.hash && isValidQuerySelectorString(route.hash)) + $(route.hash)?.[0].scrollIntoView(); + }, + callLifecycleProperty(method, ...params) { + const attr = getAttribute(attributes, method, this.context); + + if (attr) attr.evaluate(this.context)(...params); + }, + } +}; + +var routeHoc = { + css: null, + + exports: pure( + routeHoc$1 + ), + + template: null, + name: 'route-hoc' +}; + +const normalizeInitialSlash = (str) => + str[0] === SLASH ? str : `${SLASH}${str}`; +const removeTrailingSlash = (str) => + str[str.length - 1] === SLASH ? str.substr(0, str.length - 1) : str; + +const normalizeBase = (base) => { + const win = getWindow(); + const loc = win.location; + const root = loc ? `${loc.protocol}//${loc.host}` : ''; + const { pathname } = loc ? loc : {}; + + switch (true) { + // pure root url + pathname + case Boolean(base) === false: + return removeTrailingSlash(`${root}${pathname || ''}`) + // full path base + case /(www|http(s)?:)/.test(base): + return base + // hash navigation + case base[0] === HASH: + return `${root}${pathname && pathname !== SLASH ? pathname : ''}${base}` + // root url with trailing slash + case base === SLASH: + return removeTrailingSlash(root) + // custom pathname + default: + return removeTrailingSlash(`${root}${normalizeInitialSlash(base)}`) + } +}; + +function setBase(base) { + configure({ base: normalizeBase(base) }); +} + +/** + * Throw an error with a descriptive message + * @param { string } message - error message + * @param { string } cause - optional error cause object + * @returns { undefined } hoppla... at this point the program should stop working + */ +function panic(message, cause) { + throw new Error(message, { cause }) +} + +/** + * Split a string into several items separed by spaces + * @param { string } l - events list + * @returns { Array } all the events detected + * @private + */ +const split = l => l.split(/\s/); + +/** + * Set a listener for all the events received separated by spaces + * @param { HTMLElement|NodeList|Array } els - DOM node/s where the listeners will be bound + * @param { string } evList - list of events we want to bind or unbind space separated + * @param { Function } cb - listeners callback + * @param { string } method - either 'addEventListener' or 'removeEventListener' + * @param { Object } options - event options (capture, once and passive) + * @returns { undefined } + * @private + */ +function manageEvents(els, evList, cb, method, options) { + els = domToArray(els); + + split(evList).forEach((e) => { + els.forEach(el => el[method](e, cb, options || false)); + }); +} + +/** + * Set a listener for all the events received separated by spaces + * @param { HTMLElement|Array } els - DOM node/s where the listeners will be bound + * @param { string } evList - list of events we want to bind space separated + * @param { Function } cb - listeners callback + * @param { Object } options - event options (capture, once and passive) + * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function + */ +function add(els, evList, cb, options) { + manageEvents(els, evList, cb, 'addEventListener', options); + return els +} + +/** + * Remove all the listeners for the events received separated by spaces + * @param { HTMLElement|Array } els - DOM node/s where the events will be unbind + * @param { string } evList - list of events we want unbind space separated + * @param { Function } cb - listeners callback + * @param { Object } options - event options (capture, once and passive) + * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function + */ +function remove(els, evList, cb, options) { + manageEvents(els, evList, cb, 'removeEventListener', options); + return els +} + +const onWindowEvent = () => + router.push(normalizePath(String(getLocation().href))); +const onRouterPush = (path) => { + const url = path.includes(defaults.base) ? path : defaults.base + path; + const loc = getLocation(); + const hist = getHistory(); + const doc = getDocument(); + + // update the browser history only if it's necessary + if (hist && url !== loc.href) { + hist.pushState(null, doc.title, url); + } +}; +const getLinkElement = (node) => + node && !isLinkNode(node) ? getLinkElement(node.parentNode) : node; +const isLinkNode = (node) => node.nodeName === LINK_TAG_NAME; +const isCrossOriginLink = (path) => + path.indexOf(getLocation().href.match(RE_ORIGIN)[0]) === -1; +const isTargetSelfLink = (el) => + el.target && el.target !== TARGET_SELF_LINK_ATTRIBUTE; +const isEventForbidden = (event) => + (event.which && event.which !== 1) || // not left click + event.metaKey || + event.ctrlKey || + event.shiftKey || // or meta keys + event.defaultPrevented; // or default prevented +const isForbiddenLink = (el) => + !el || + !isLinkNode(el) || // not A tag + has(el, DOWNLOAD_LINK_ATTRIBUTE) || // has download attr + !has(el, HREF_LINK_ATTRIBUTE) || // has no href attr + isTargetSelfLink(el) || + isCrossOriginLink(el.href); +const normalizePath = (path) => path.replace(defaults.base, ''); +const isInBase = (path) => !defaults.base || path.includes(defaults.base); + +/** + * Callback called anytime something will be clicked on the page + * @param {Event} event - click event + * @returns {undefined} void method + */ +const onClick = (event) => { + if (isEventForbidden(event)) return + + const el = getLinkElement(event.target); + + if (isForbiddenLink(el) || !isInBase(el.href)) return + + event.preventDefault(); + + router.push(normalizePath(el.href)); +}; + +/** + * Link the rawth router to the DOM events + * @param { HTMLElement } container - DOM node where the links are located + * @returns {Function} teardown function + */ +function initDomListeners(container) { + const win = getWindow(); + const root = container || getDocument(); + + if (win) { + add(win, WINDOW_EVENTS, onWindowEvent); + add(root, CLICK_EVENT, onClick); + } + + router.on.value(onRouterPush); + + return () => { + if (win) { + remove(win, WINDOW_EVENTS, onWindowEvent); + remove(root, CLICK_EVENT, onClick); + } + + router.off.value(onRouterPush); + } +} + +const BASE_ATTRIBUTE_NAME = 'base'; +const INITIAL_ROUTE = 'initialRoute'; +const ON_STARTED_ATTRIBUTE_NAME = 'onStarted'; + +const routerHoc$1 = ({ slots, attributes, props }) => { + if (routerHoc$1.wasInitialized) + panic('Multiple components are not supported'); + + return { + slot: null, + el: null, + teardown: null, + mount(el, context) { + const initialRouteAttr = getAttribute(attributes, INITIAL_ROUTE, context); + const initialRoute = initialRouteAttr + ? initialRouteAttr.evaluate(context) + : null; + const currentRoute = getCurrentRoute(); + const onFirstRoute = () => { + this.createSlot(context); + router.off.value(onFirstRoute); + }; + routerHoc$1.wasInitialized = true; + + this.el = el; + this.teardown = initDomListeners(this.root); + + this.setBase(context); + + // mount the slots only if the current route was defined + if (currentRoute && !initialRoute) { + this.createSlot(context); + } else { + router.on.value(onFirstRoute); + router.push(initialRoute || window.location.href); + } + }, + createSlot(context) { + if (!slots || !slots.length) return + const onStartedAttr = getAttribute( + attributes, + ON_STARTED_ATTRIBUTE_NAME, + context, + ); + + this.slot = createDefaultSlot(); + + this.slot.mount( + this.el, + { + slots, + }, + context, + ); + + if (onStartedAttr) { + onStartedAttr.evaluate(context)(getCurrentRoute()); + } + }, + update(context) { + this.setBase(context); + + // defer the updates to avoid internal recursive update calls + // see https://github.com/riot/route/issues/148 + if (this.slot) { + cancelDefer(this.deferred); + + this.deferred = defer(() => { + this.slot.update({}, context); + }); + } + }, + unmount(...args) { + this.teardown(); + routerHoc$1.wasInitialized = false; + + if (this.slot) { + this.slot.unmount(...args); + } + }, + getBase(context) { + const baseAttr = getAttribute(attributes, BASE_ATTRIBUTE_NAME, context); + + return baseAttr + ? baseAttr.evaluate(context) + : this.el.getAttribute(BASE_ATTRIBUTE_NAME) || '/' + }, + setBase(context) { + setBase(props ? props.base : this.getBase(context)); + }, + } +}; + +// flag to avoid multiple router instances +routerHoc$1.wasInitialized = false; + +var routerHoc = { + css: null, + + exports: pure( + routerHoc$1 + ), + + template: null, + name: 'router-hoc' +}; + +export { routeHoc as Route, routerHoc as Router, configure, createURLStreamPipe, defaults, getCurrentRoute, initDomListeners, match, createRoute as route, router, setBase, toPath, toRegexp, toURL }; diff --git a/node_modules/@riotjs/route/index.standalone.js b/node_modules/@riotjs/route/index.standalone.js new file mode 100644 index 0000000..6f371df --- /dev/null +++ b/node_modules/@riotjs/route/index.standalone.js @@ -0,0 +1,1039 @@ +import 'riot'; + +/** + * Tokenize input string. + */ +function lexer(str) { + var tokens = []; + var i = 0; + while (i < str.length) { + var char = str[i]; + if (char === "*" || char === "+" || char === "?") { + tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); + continue; + } + if (char === "\\") { + tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); + continue; + } + if (char === "{") { + tokens.push({ type: "OPEN", index: i, value: str[i++] }); + continue; + } + if (char === "}") { + tokens.push({ type: "CLOSE", index: i, value: str[i++] }); + continue; + } + if (char === ":") { + var name = ""; + var j = i + 1; + while (j < str.length) { + var code = str.charCodeAt(j); + if ( + // `0-9` + (code >= 48 && code <= 57) || + // `A-Z` + (code >= 65 && code <= 90) || + // `a-z` + (code >= 97 && code <= 122) || + // `_` + code === 95) { + name += str[j++]; + continue; + } + break; + } + if (!name) + throw new TypeError("Missing parameter name at ".concat(i)); + tokens.push({ type: "NAME", index: i, value: name }); + i = j; + continue; + } + if (char === "(") { + var count = 1; + var pattern = ""; + var j = i + 1; + if (str[j] === "?") { + throw new TypeError("Pattern cannot start with \"?\" at ".concat(j)); + } + while (j < str.length) { + if (str[j] === "\\") { + pattern += str[j++] + str[j++]; + continue; + } + if (str[j] === ")") { + count--; + if (count === 0) { + j++; + break; + } + } + else if (str[j] === "(") { + count++; + if (str[j + 1] !== "?") { + throw new TypeError("Capturing groups are not allowed at ".concat(j)); + } + } + pattern += str[j++]; + } + if (count) + throw new TypeError("Unbalanced pattern at ".concat(i)); + if (!pattern) + throw new TypeError("Missing pattern at ".concat(i)); + tokens.push({ type: "PATTERN", index: i, value: pattern }); + i = j; + continue; + } + tokens.push({ type: "CHAR", index: i, value: str[i++] }); + } + tokens.push({ type: "END", index: i, value: "" }); + return tokens; +} +/** + * Parse a string for the raw tokens. + */ +function parse(str, options) { + if (options === void 0) { options = {}; } + var tokens = lexer(str); + var _a = options.prefixes, prefixes = _a === void 0 ? "./" : _a; + var defaultPattern = "[^".concat(escapeString(options.delimiter || "/#?"), "]+?"); + var result = []; + var key = 0; + var i = 0; + var path = ""; + var tryConsume = function (type) { + if (i < tokens.length && tokens[i].type === type) + return tokens[i++].value; + }; + var mustConsume = function (type) { + var value = tryConsume(type); + if (value !== undefined) + return value; + var _a = tokens[i], nextType = _a.type, index = _a.index; + throw new TypeError("Unexpected ".concat(nextType, " at ").concat(index, ", expected ").concat(type)); + }; + var consumeText = function () { + var result = ""; + var value; + while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { + result += value; + } + return result; + }; + while (i < tokens.length) { + var char = tryConsume("CHAR"); + var name = tryConsume("NAME"); + var pattern = tryConsume("PATTERN"); + if (name || pattern) { + var prefix = char || ""; + if (prefixes.indexOf(prefix) === -1) { + path += prefix; + prefix = ""; + } + if (path) { + result.push(path); + path = ""; + } + result.push({ + name: name || key++, + prefix: prefix, + suffix: "", + pattern: pattern || defaultPattern, + modifier: tryConsume("MODIFIER") || "", + }); + continue; + } + var value = char || tryConsume("ESCAPED_CHAR"); + if (value) { + path += value; + continue; + } + if (path) { + result.push(path); + path = ""; + } + var open = tryConsume("OPEN"); + if (open) { + var prefix = consumeText(); + var name_1 = tryConsume("NAME") || ""; + var pattern_1 = tryConsume("PATTERN") || ""; + var suffix = consumeText(); + mustConsume("CLOSE"); + result.push({ + name: name_1 || (pattern_1 ? key++ : ""), + pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1, + prefix: prefix, + suffix: suffix, + modifier: tryConsume("MODIFIER") || "", + }); + continue; + } + mustConsume("END"); + } + return result; +} +/** + * Compile a string to a template function for the path. + */ +function compile(str, options) { + return tokensToFunction(parse(str, options), options); +} +/** + * Expose a method for transforming tokens into the path function. + */ +function tokensToFunction(tokens, options) { + if (options === void 0) { options = {}; } + var reFlags = flags(options); + var _a = options.encode, encode = _a === void 0 ? function (x) { return x; } : _a, _b = options.validate, validate = _b === void 0 ? true : _b; + // Compile all the tokens into regexps. + var matches = tokens.map(function (token) { + if (typeof token === "object") { + return new RegExp("^(?:".concat(token.pattern, ")$"), reFlags); + } + }); + return function (data) { + var path = ""; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (typeof token === "string") { + path += token; + continue; + } + var value = data ? data[token.name] : undefined; + var optional = token.modifier === "?" || token.modifier === "*"; + var repeat = token.modifier === "*" || token.modifier === "+"; + if (Array.isArray(value)) { + if (!repeat) { + throw new TypeError("Expected \"".concat(token.name, "\" to not repeat, but got an array")); + } + if (value.length === 0) { + if (optional) + continue; + throw new TypeError("Expected \"".concat(token.name, "\" to not be empty")); + } + for (var j = 0; j < value.length; j++) { + var segment = encode(value[j], token); + if (validate && !matches[i].test(segment)) { + throw new TypeError("Expected all \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\"")); + } + path += token.prefix + segment + token.suffix; + } + continue; + } + if (typeof value === "string" || typeof value === "number") { + var segment = encode(String(value), token); + if (validate && !matches[i].test(segment)) { + throw new TypeError("Expected \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\"")); + } + path += token.prefix + segment + token.suffix; + continue; + } + if (optional) + continue; + var typeOfMessage = repeat ? "an array" : "a string"; + throw new TypeError("Expected \"".concat(token.name, "\" to be ").concat(typeOfMessage)); + } + return path; + }; +} +/** + * Escape a regular expression string. + */ +function escapeString(str) { + return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); +} +/** + * Get the flags for a regexp from the options. + */ +function flags(options) { + return options && options.sensitive ? "" : "i"; +} +/** + * Pull out keys from a regexp. + */ +function regexpToRegexp(path, keys) { + if (!keys) + return path; + var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; + var index = 0; + var execResult = groupsRegex.exec(path.source); + while (execResult) { + keys.push({ + // Use parenthesized substring match if available, index otherwise + name: execResult[1] || index++, + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }); + execResult = groupsRegex.exec(path.source); + } + return path; +} +/** + * Transform an array into a regexp. + */ +function arrayToRegexp(paths, keys, options) { + var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source; }); + return new RegExp("(?:".concat(parts.join("|"), ")"), flags(options)); +} +/** + * Create a path regexp from string input. + */ +function stringToRegexp(path, keys, options) { + return tokensToRegexp(parse(path, options), keys, options); +} +/** + * Expose a function for taking tokens and returning a RegExp. + */ +function tokensToRegexp(tokens, keys, options) { + if (options === void 0) { options = {}; } + var _a = options.strict, strict = _a === void 0 ? false : _a, _b = options.start, start = _b === void 0 ? true : _b, _c = options.end, end = _c === void 0 ? true : _c, _d = options.encode, encode = _d === void 0 ? function (x) { return x; } : _d, _e = options.delimiter, delimiter = _e === void 0 ? "/#?" : _e, _f = options.endsWith, endsWith = _f === void 0 ? "" : _f; + var endsWithRe = "[".concat(escapeString(endsWith), "]|$"); + var delimiterRe = "[".concat(escapeString(delimiter), "]"); + var route = start ? "^" : ""; + // Iterate over the tokens and create our regexp string. + for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { + var token = tokens_1[_i]; + if (typeof token === "string") { + route += escapeString(encode(token)); + } + else { + var prefix = escapeString(encode(token.prefix)); + var suffix = escapeString(encode(token.suffix)); + if (token.pattern) { + if (keys) + keys.push(token); + if (prefix || suffix) { + if (token.modifier === "+" || token.modifier === "*") { + var mod = token.modifier === "*" ? "?" : ""; + route += "(?:".concat(prefix, "((?:").concat(token.pattern, ")(?:").concat(suffix).concat(prefix, "(?:").concat(token.pattern, "))*)").concat(suffix, ")").concat(mod); + } + else { + route += "(?:".concat(prefix, "(").concat(token.pattern, ")").concat(suffix, ")").concat(token.modifier); + } + } + else { + if (token.modifier === "+" || token.modifier === "*") { + route += "((?:".concat(token.pattern, ")").concat(token.modifier, ")"); + } + else { + route += "(".concat(token.pattern, ")").concat(token.modifier); + } + } + } + else { + route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier); + } + } + } + if (end) { + if (!strict) + route += "".concat(delimiterRe, "?"); + route += !options.endsWith ? "$" : "(?=".concat(endsWithRe, ")"); + } + else { + var endToken = tokens[tokens.length - 1]; + var isEndDelimited = typeof endToken === "string" + ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 + : endToken === undefined; + if (!strict) { + route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?"); + } + if (!isEndDelimited) { + route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")"); + } + } + 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 }]`. + */ +function pathToRegexp(path, keys, options) { + if (path instanceof RegExp) + return regexpToRegexp(path, keys); + if (Array.isArray(path)) + return arrayToRegexp(path, keys, options); + return stringToRegexp(path, keys, options); +} + +/** + * Cancel token + * @private + * @type { Symbol } + */ +const CANCEL = Symbol(); + +/** + * Helper that can be returned by ruit function to cancel the tasks chain + * @returns { Symbol } internal private constant + * @example + * + * ruit( + * 100, + * num => Math.random() * num + * num => num > 50 ? ruit.cancel() : num + * num => num - 2 + * ).then(result => { + * console.log(result) // here we will get only number lower than 50 + * }) + * + */ +ruit.cancel = () => CANCEL; + +/** + * The same as ruit() but with the arguments inverted from right to left + * @param { * } tasks - list of tasks to process sequentially + * @returns { Promise } a promise containing the result of the whole chain + * @example + * + * const curry = f => a => b => f(a, b) + * const add = (a, b) => a + b + * + * const addOne = curry(add)(1) + * + * const squareAsync = (num) => { + * return new Promise(r => { + * setTimeout(r, 500, num * 2) + * }) + * } + * + * // a -> a + a -> a * 2 + * // basically from right to left: 1 => 1 + 1 => 2 * 2 + * ruit.compose(squareAsync, addOne, 1).then(result => console.log(result)) // 4 + */ +ruit.compose = (...tasks) => ruit(...tasks.reverse()); + +/** + * Serialize a list of sync and async tasks from left to right + * @param { * } tasks - list of tasks to process sequentially + * @returns { Promise } a promise containing the result of the whole chain + * @example + * + * const curry = f => a => b => f(a, b) + * const add = (a, b) => a + b + * + * const addOne = curry(add)(1) + * + * const squareAsync = (num) => { + * return new Promise(r => { + * setTimeout(r, 500, num * 2) + * }) + * } + * + * // a -> a + a -> a * 2 + * // basically from left to right: 1 => 1 + 1 => 2 * 2 + * ruit(1, addOne, squareAsync).then(result => console.log(result)) // 4 + */ +function ruit(...tasks) { + return new Promise((resolve, reject) => { + return (function run(queue, result) { + if (!queue.length) return resolve(result) + + const [task, ...rest] = queue; + const value = typeof task === 'function' ? task(result) : task; + const done = v => run(rest, v); + + // check against nil values + if (value != null) { + if (value === CANCEL) return + if (value.then) return value.then(done, reject) + } + + return Promise.resolve(done(value)) + })(tasks) + }) +} + +// Store the erre the API methods to handle the plugins installation +const API_METHODS = new Set(); +const UNSUBSCRIBE_SYMBOL = Symbol(); +const UNSUBSCRIBE_METHOD = 'off'; +const CANCEL_METHOD = 'cancel'; + +/** + * Factory function to create the stream generator + * @private + * @param {Set} modifiers - stream input modifiers + * @returns {Generator} the stream generator + */ +function createStream(modifiers) { + const stream = (function *stream() { + while (true) { + // get the initial stream value + const input = yield; + + // run the input sequence + yield ruit(input, ...modifiers); + } + })(); + + // start the stream + stream.next(); + + return stream +} + +/** + * Dispatch a value to several listeners + * @private + * @param {Set} callbacks - callbacks collection + * @param {*} value - anything + * @returns {Set} the callbacks received + */ +function dispatch(callbacks, value) { + callbacks.forEach(f => { + // unsubscribe the callback if erre.unsubscribe() will be returned + if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f); + }); + + return callbacks +} + +/** + * Throw a panic error + * @param {string} message - error message + * @returns {Error} an error object + */ +function panic$1(message) { + throw new Error(message) +} + +/** + * Install an erre plugin adding it to the API + * @param {string} name - plugin name + * @param {Function} fn - new erre API method + * @returns {Function} return the erre function + */ +erre.install = function(name, fn) { + if (!name || typeof name !== 'string') + panic$1('Please provide a name (as string) for your erre plugin'); + if (!fn || typeof fn !== 'function') + panic$1('Please provide a function for your erre plugin'); + + if (API_METHODS.has(name)) { + panic$1(`The ${name} is already part of the erre API, please provide a different name`); + } else { + erre[name] = fn; + API_METHODS.add(name); + } + + return erre +}; + +// alias for ruit canel to stop a stream chain +erre.install(CANCEL_METHOD, ruit.cancel); + +// unsubscribe helper +erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL); + +/** + * Stream constuction function + * @param {...Function} fns - stream modifiers + * @returns {Object} erre instance + */ +function erre(...fns) { + const + [success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)], + generator = createStream(modifiers), + stream = Object.create(generator), + addToCollection = (collection) => (fn) => collection.add(fn) && stream, + deleteFromCollection = (collection) => (fn) => collection.delete(fn) ? stream + : panic$1('Couldn\'t remove handler passed by reference'); + + return Object.assign(stream, { + on: Object.freeze({ + value: addToCollection(success), + error: addToCollection(error), + end: addToCollection(end) + }), + off: Object.freeze({ + value: deleteFromCollection(success), + error: deleteFromCollection(error), + end: deleteFromCollection(end) + }), + connect: addToCollection(modifiers), + push(input) { + const { value, done } = stream.next(input); + + // dispatch the stream events + if (!done) { + value.then( + res => dispatch(success, res), + err => dispatch(error, err) + ); + } + + return stream + }, + end() { + // kill the stream + generator.return(); + // dispatch the end event + dispatch(end) + // clean up all the collections + ;[success, error, end, modifiers].forEach(el => el.clear()); + + return stream + }, + fork() { + return erre(...modifiers) + }, + next(input) { + // get the input and run eventually the promise + const result = generator.next(input); + + // pause to the next iteration + generator.next(); + + return result + } + }) +} + +const isString = str => typeof str === 'string'; +const parseURL = (...args) => new URL(...args); + +/** + * Replace the base path from a path + * @param {string} path - router path string + * @returns {string} path cleaned up without the base + */ +const replaceBase = path => path.replace(defaults.base, ''); + +/** + * Try to match the current path or skip it + * @param {RegExp} pathRegExp - target path transformed by pathToRegexp + * @returns {string|Symbol} if the path match we return it otherwise we cancel the stream + */ +const matchOrSkip = pathRegExp => path => match(path, pathRegExp) ? path : erre.cancel(); + +/** + * Combine 2 streams connecting the events of dispatcherStream to the receiverStream + * @param {Stream} dispatcherStream - main stream dispatching events + * @param {Stream} receiverStream - sub stream receiving events from the dispatcher + * @returns {Stream} receiverStream + */ +const joinStreams = (dispatcherStream, receiverStream) => { + dispatcherStream.on.value(receiverStream.push); + + receiverStream.on.end(() => { + dispatcherStream.off.value(receiverStream.push); + }); + + return receiverStream +}; + +/** + * Error handling function + * @param {Error} error - error to catch + * @returns {void} + */ +/* c8 ignore start */ +const panic = error => { + if (defaults.silentErrors) return + + throw new Error(error) +}; +/* c8 ignore stop */ + +// make sure that the router will always receive strings params +const filterStrings = str => isString(str) ? str : erre.cancel(); + +// create the streaming router +const router = erre(filterStrings).on.error(panic); // cast the values of this stream always to string + +/** + * Merge the user options with the defaults + * @param {Object} options - custom user options + * @returns {Object} options object merged with defaults + */ +const mergeOptions = options => ({...defaults, ...options}); + +/* @type {object} general configuration object */ +const defaults = { + base: 'https://localhost', + silentErrors: false, + // pathToRegexp options + sensitive: false, + strict: false, + end: true, + start: true, + delimiter: '/#?', + encode: undefined, + endsWith: undefined, + prefixes: './' +}; + +/** + * Configure the router options overriding the defaults + * @param {Object} options - custom user options to override + * @returns {Object} new defaults + */ +const configure = (options) => { + Object.entries(options).forEach(([key, value]) => { + if (Object.hasOwn(defaults, key)) defaults[key] = value; + }); + + return defaults +}; + + + +/* {@link https://github.com/pillarjs/path-to-regexp#usage} */ +const toRegexp = (path, keys, options) => pathToRegexp(path, keys, mergeOptions(options)); + +/** + * Convert a router entry to a real path computing the url parameters + * @param {string} path - router path string + * @param {Object} params - named matched parameters + * @param {Object} options - pathToRegexp options object + * @returns {string} computed url string + */ +const toPath = (path, params, options) => compile(path, mergeOptions(options))(params); + +/** + * Parse a string path generating an object containing + * @param {string} path - target path + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @param {Object} options - object containing the base path + * @returns {URL} url object enhanced with the `match` attribute + */ +const toURL = (path, pathRegExp, options = {}) => { + const {base} = mergeOptions(options); + const [, ...params] = pathRegExp.exec(path); + const url = parseURL(path, base); + + // extend the url object adding the matched params + url.params = params.reduce((acc, param, index) => { + const key = options.keys && options.keys[index]; + if (key) acc[key.name] = param ? decodeURIComponent(param) : param; + return acc + }, {}); + + return url +}; + +/** + * Return true if a path will be matched + * @param {string} path - target path + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @returns {boolean} true if the path matches the regexp + */ +const match = (path, pathRegExp) => pathRegExp.test(path); + +/** + * Factory function to create an sequence of functions to pass to erre.js + * This function will be used in the erre stream + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @param {Object} options - pathToRegexp options object + * @returns {Array} a functions array that will be used as stream pipe for erre.js + */ +const createURLStreamPipe = (pathRegExp, options) => [ + decodeURI, + replaceBase, + matchOrSkip(pathRegExp), + path => toURL(path, pathRegExp, options) +]; + +/** + * Create a fork of the main router stream + * @param {string} path - route to match + * @param {Object} options - pathToRegexp options object + * @returns {Stream} new route stream + */ +function createRoute(path, options) { + const keys = []; + const pathRegExp = pathToRegexp(path, keys, options); + const URLStream = erre(...createURLStreamPipe(pathRegExp, { + ...options, + keys + })); + + return joinStreams(router, URLStream).on.error(panic) +} + +var routeHoc = {}; + +var routerHoc = {}; + +const getCurrentRoute = ((currentRoute) => { + // listen the route changes events to store the current route + router.on.value((r) => (currentRoute = r)); + + return () => { + return currentRoute + } +})(null); + +const WINDOW_EVENTS = 'popstate'; +const CLICK_EVENT = 'click'; +const DOWNLOAD_LINK_ATTRIBUTE = 'download'; +const HREF_LINK_ATTRIBUTE = 'href'; +const TARGET_SELF_LINK_ATTRIBUTE = '_self'; +const LINK_TAG_NAME = 'A'; +const HASH = '#'; +const SLASH = '/'; +const RE_ORIGIN = /^.+?\/\/+[^/]+/; + +/** + * Converts any DOM node/s to a loopable array + * @param { HTMLElement|NodeList } els - single html element or a node list + * @returns { Array } always a loopable object + */ +function domToArray(els) { + // can this object be already looped? + if (!Array.isArray(els)) { + // is it a node list? + if ( + /^\[object (HTMLCollection|NodeList|Object)\]$/ + .test(Object.prototype.toString.call(els)) + && typeof els.length === 'number' + ) + return Array.from(els) + else + // if it's a single node + // it will be returned as "array" with one single entry + return [els] + } + // this object could be looped out of the box + return els +} + +/** + * Split a string into several items separed by spaces + * @param { string } l - events list + * @returns { Array } all the events detected + * @private + */ +const split = l => l.split(/\s/); + +/** + * Set a listener for all the events received separated by spaces + * @param { HTMLElement|NodeList|Array } els - DOM node/s where the listeners will be bound + * @param { string } evList - list of events we want to bind or unbind space separated + * @param { Function } cb - listeners callback + * @param { string } method - either 'addEventListener' or 'removeEventListener' + * @param { Object } options - event options (capture, once and passive) + * @returns { undefined } + * @private + */ +function manageEvents(els, evList, cb, method, options) { + els = domToArray(els); + + split(evList).forEach((e) => { + els.forEach(el => el[method](e, cb, options || false)); + }); +} + +/** + * Set a listener for all the events received separated by spaces + * @param { HTMLElement|Array } els - DOM node/s where the listeners will be bound + * @param { string } evList - list of events we want to bind space separated + * @param { Function } cb - listeners callback + * @param { Object } options - event options (capture, once and passive) + * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function + */ +function add(els, evList, cb, options) { + manageEvents(els, evList, cb, 'addEventListener', options); + return els +} + +/** + * Remove all the listeners for the events received separated by spaces + * @param { HTMLElement|Array } els - DOM node/s where the events will be unbind + * @param { string } evList - list of events we want unbind space separated + * @param { Function } cb - listeners callback + * @param { Object } options - event options (capture, once and passive) + * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function + */ +function remove(els, evList, cb, options) { + manageEvents(els, evList, cb, 'removeEventListener', options); + return els +} + +const getGlobal = () => getWindow() || global; +const getWindow = () => (typeof window === 'undefined' ? null : window); +const getDocument = () => + typeof document === 'undefined' ? null : document; +const getHistory = () => + typeof history === 'undefined' ? null : history; +const getLocation = () => { + const win = getWindow(); + return win ? win.location : {} +}; + +(() => { + const globalScope = getGlobal(); + + return globalScope.requestAnimationFrame || globalScope.setTimeout +})(); + +(() => { + const globalScope = getGlobal(); + + return globalScope.cancelAnimationFrame || globalScope.clearTimeout +})(); + +/** + * Normalize the return values, in case of a single value we avoid to return an array + * @param { Array } values - list of values we want to return + * @returns { Array|string|boolean } either the whole list of values or the single one found + * @private + */ +const normalize = values => values.length === 1 ? values[0] : values; + +/** + * Parse all the nodes received to get/remove/check their attributes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes + * @param { string } method - method that will be used to parse the attributes + * @returns { Array|string } result of the parsing in a list or a single value + * @private + */ +function parseNodes(els, name, method) { + const names = typeof name === 'string' ? [name] : name; + return normalize(domToArray(els).map(el => { + return normalize(names.map(n => el[method](n))) + })) +} + +/** + * Set any attribute on a single or a list of DOM nodes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes to detect + * @returns { boolean|Array } true or false or an array of boolean values + * @example + * + * import { has } from 'bianco.attr' + * + * has(img, 'width') // false + * + * // or also + * has(img, ['width', 'height']) // => [false, false] + * + * // or also + * has([img1, img2], ['width', 'height']) // => [[false, false], [false, false]] + */ +function has(els, name) { + return parseNodes(els, name, 'hasAttribute') +} + +const onWindowEvent = () => + router.push(normalizePath(String(getLocation().href))); +const onRouterPush = (path) => { + const url = path.includes(defaults.base) ? path : defaults.base + path; + const loc = getLocation(); + const hist = getHistory(); + const doc = getDocument(); + + // update the browser history only if it's necessary + if (hist && url !== loc.href) { + hist.pushState(null, doc.title, url); + } +}; +const getLinkElement = (node) => + node && !isLinkNode(node) ? getLinkElement(node.parentNode) : node; +const isLinkNode = (node) => node.nodeName === LINK_TAG_NAME; +const isCrossOriginLink = (path) => + path.indexOf(getLocation().href.match(RE_ORIGIN)[0]) === -1; +const isTargetSelfLink = (el) => + el.target && el.target !== TARGET_SELF_LINK_ATTRIBUTE; +const isEventForbidden = (event) => + (event.which && event.which !== 1) || // not left click + event.metaKey || + event.ctrlKey || + event.shiftKey || // or meta keys + event.defaultPrevented; // or default prevented +const isForbiddenLink = (el) => + !el || + !isLinkNode(el) || // not A tag + has(el, DOWNLOAD_LINK_ATTRIBUTE) || // has download attr + !has(el, HREF_LINK_ATTRIBUTE) || // has no href attr + isTargetSelfLink(el) || + isCrossOriginLink(el.href); +const normalizePath = (path) => path.replace(defaults.base, ''); +const isInBase = (path) => !defaults.base || path.includes(defaults.base); + +/** + * Callback called anytime something will be clicked on the page + * @param {Event} event - click event + * @returns {undefined} void method + */ +const onClick = (event) => { + if (isEventForbidden(event)) return + + const el = getLinkElement(event.target); + + if (isForbiddenLink(el) || !isInBase(el.href)) return + + event.preventDefault(); + + router.push(normalizePath(el.href)); +}; + +/** + * Link the rawth router to the DOM events + * @param { HTMLElement } container - DOM node where the links are located + * @returns {Function} teardown function + */ +function initDomListeners(container) { + const win = getWindow(); + const root = container || getDocument(); + + if (win) { + add(win, WINDOW_EVENTS, onWindowEvent); + add(root, CLICK_EVENT, onClick); + } + + router.on.value(onRouterPush); + + return () => { + if (win) { + remove(win, WINDOW_EVENTS, onWindowEvent); + remove(root, CLICK_EVENT, onClick); + } + + router.off.value(onRouterPush); + } +} + +const normalizeInitialSlash = (str) => + str[0] === SLASH ? str : `${SLASH}${str}`; +const removeTrailingSlash = (str) => + str[str.length - 1] === SLASH ? str.substr(0, str.length - 1) : str; + +const normalizeBase = (base) => { + const win = getWindow(); + const loc = win.location; + const root = loc ? `${loc.protocol}//${loc.host}` : ''; + const { pathname } = loc ? loc : {}; + + switch (true) { + // pure root url + pathname + case Boolean(base) === false: + return removeTrailingSlash(`${root}${pathname || ''}`) + // full path base + case /(www|http(s)?:)/.test(base): + return base + // hash navigation + case base[0] === HASH: + return `${root}${pathname && pathname !== SLASH ? pathname : ''}${base}` + // root url with trailing slash + case base === SLASH: + return removeTrailingSlash(root) + // custom pathname + default: + return removeTrailingSlash(`${root}${normalizeInitialSlash(base)}`) + } +}; + +function setBase(base) { + configure({ base: normalizeBase(base) }); +} + +export { routeHoc as Route, routerHoc as Router, configure, createURLStreamPipe, defaults, getCurrentRoute, initDomListeners, match, createRoute as route, router, setBase, toPath, toRegexp, toURL }; diff --git a/node_modules/@riotjs/route/index.standalone.umd.js b/node_modules/@riotjs/route/index.standalone.umd.js new file mode 100644 index 0000000..bb481be --- /dev/null +++ b/node_modules/@riotjs/route/index.standalone.umd.js @@ -0,0 +1,1058 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('riot')) : + typeof define === 'function' && define.amd ? define(['exports', 'riot'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.route = {})); +})(this, (function (exports) { 'use strict'; + + /** + * Tokenize input string. + */ + function lexer(str) { + var tokens = []; + var i = 0; + while (i < str.length) { + var char = str[i]; + if (char === "*" || char === "+" || char === "?") { + tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); + continue; + } + if (char === "\\") { + tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); + continue; + } + if (char === "{") { + tokens.push({ type: "OPEN", index: i, value: str[i++] }); + continue; + } + if (char === "}") { + tokens.push({ type: "CLOSE", index: i, value: str[i++] }); + continue; + } + if (char === ":") { + var name = ""; + var j = i + 1; + while (j < str.length) { + var code = str.charCodeAt(j); + if ( + // `0-9` + (code >= 48 && code <= 57) || + // `A-Z` + (code >= 65 && code <= 90) || + // `a-z` + (code >= 97 && code <= 122) || + // `_` + code === 95) { + name += str[j++]; + continue; + } + break; + } + if (!name) + throw new TypeError("Missing parameter name at ".concat(i)); + tokens.push({ type: "NAME", index: i, value: name }); + i = j; + continue; + } + if (char === "(") { + var count = 1; + var pattern = ""; + var j = i + 1; + if (str[j] === "?") { + throw new TypeError("Pattern cannot start with \"?\" at ".concat(j)); + } + while (j < str.length) { + if (str[j] === "\\") { + pattern += str[j++] + str[j++]; + continue; + } + if (str[j] === ")") { + count--; + if (count === 0) { + j++; + break; + } + } + else if (str[j] === "(") { + count++; + if (str[j + 1] !== "?") { + throw new TypeError("Capturing groups are not allowed at ".concat(j)); + } + } + pattern += str[j++]; + } + if (count) + throw new TypeError("Unbalanced pattern at ".concat(i)); + if (!pattern) + throw new TypeError("Missing pattern at ".concat(i)); + tokens.push({ type: "PATTERN", index: i, value: pattern }); + i = j; + continue; + } + tokens.push({ type: "CHAR", index: i, value: str[i++] }); + } + tokens.push({ type: "END", index: i, value: "" }); + return tokens; + } + /** + * Parse a string for the raw tokens. + */ + function parse(str, options) { + if (options === void 0) { options = {}; } + var tokens = lexer(str); + var _a = options.prefixes, prefixes = _a === void 0 ? "./" : _a; + var defaultPattern = "[^".concat(escapeString(options.delimiter || "/#?"), "]+?"); + var result = []; + var key = 0; + var i = 0; + var path = ""; + var tryConsume = function (type) { + if (i < tokens.length && tokens[i].type === type) + return tokens[i++].value; + }; + var mustConsume = function (type) { + var value = tryConsume(type); + if (value !== undefined) + return value; + var _a = tokens[i], nextType = _a.type, index = _a.index; + throw new TypeError("Unexpected ".concat(nextType, " at ").concat(index, ", expected ").concat(type)); + }; + var consumeText = function () { + var result = ""; + var value; + while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { + result += value; + } + return result; + }; + while (i < tokens.length) { + var char = tryConsume("CHAR"); + var name = tryConsume("NAME"); + var pattern = tryConsume("PATTERN"); + if (name || pattern) { + var prefix = char || ""; + if (prefixes.indexOf(prefix) === -1) { + path += prefix; + prefix = ""; + } + if (path) { + result.push(path); + path = ""; + } + result.push({ + name: name || key++, + prefix: prefix, + suffix: "", + pattern: pattern || defaultPattern, + modifier: tryConsume("MODIFIER") || "", + }); + continue; + } + var value = char || tryConsume("ESCAPED_CHAR"); + if (value) { + path += value; + continue; + } + if (path) { + result.push(path); + path = ""; + } + var open = tryConsume("OPEN"); + if (open) { + var prefix = consumeText(); + var name_1 = tryConsume("NAME") || ""; + var pattern_1 = tryConsume("PATTERN") || ""; + var suffix = consumeText(); + mustConsume("CLOSE"); + result.push({ + name: name_1 || (pattern_1 ? key++ : ""), + pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1, + prefix: prefix, + suffix: suffix, + modifier: tryConsume("MODIFIER") || "", + }); + continue; + } + mustConsume("END"); + } + return result; + } + /** + * Compile a string to a template function for the path. + */ + function compile(str, options) { + return tokensToFunction(parse(str, options), options); + } + /** + * Expose a method for transforming tokens into the path function. + */ + function tokensToFunction(tokens, options) { + if (options === void 0) { options = {}; } + var reFlags = flags(options); + var _a = options.encode, encode = _a === void 0 ? function (x) { return x; } : _a, _b = options.validate, validate = _b === void 0 ? true : _b; + // Compile all the tokens into regexps. + var matches = tokens.map(function (token) { + if (typeof token === "object") { + return new RegExp("^(?:".concat(token.pattern, ")$"), reFlags); + } + }); + return function (data) { + var path = ""; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (typeof token === "string") { + path += token; + continue; + } + var value = data ? data[token.name] : undefined; + var optional = token.modifier === "?" || token.modifier === "*"; + var repeat = token.modifier === "*" || token.modifier === "+"; + if (Array.isArray(value)) { + if (!repeat) { + throw new TypeError("Expected \"".concat(token.name, "\" to not repeat, but got an array")); + } + if (value.length === 0) { + if (optional) + continue; + throw new TypeError("Expected \"".concat(token.name, "\" to not be empty")); + } + for (var j = 0; j < value.length; j++) { + var segment = encode(value[j], token); + if (validate && !matches[i].test(segment)) { + throw new TypeError("Expected all \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\"")); + } + path += token.prefix + segment + token.suffix; + } + continue; + } + if (typeof value === "string" || typeof value === "number") { + var segment = encode(String(value), token); + if (validate && !matches[i].test(segment)) { + throw new TypeError("Expected \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\"")); + } + path += token.prefix + segment + token.suffix; + continue; + } + if (optional) + continue; + var typeOfMessage = repeat ? "an array" : "a string"; + throw new TypeError("Expected \"".concat(token.name, "\" to be ").concat(typeOfMessage)); + } + return path; + }; + } + /** + * Escape a regular expression string. + */ + function escapeString(str) { + return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); + } + /** + * Get the flags for a regexp from the options. + */ + function flags(options) { + return options && options.sensitive ? "" : "i"; + } + /** + * Pull out keys from a regexp. + */ + function regexpToRegexp(path, keys) { + if (!keys) + return path; + var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; + var index = 0; + var execResult = groupsRegex.exec(path.source); + while (execResult) { + keys.push({ + // Use parenthesized substring match if available, index otherwise + name: execResult[1] || index++, + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }); + execResult = groupsRegex.exec(path.source); + } + return path; + } + /** + * Transform an array into a regexp. + */ + function arrayToRegexp(paths, keys, options) { + var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source; }); + return new RegExp("(?:".concat(parts.join("|"), ")"), flags(options)); + } + /** + * Create a path regexp from string input. + */ + function stringToRegexp(path, keys, options) { + return tokensToRegexp(parse(path, options), keys, options); + } + /** + * Expose a function for taking tokens and returning a RegExp. + */ + function tokensToRegexp(tokens, keys, options) { + if (options === void 0) { options = {}; } + var _a = options.strict, strict = _a === void 0 ? false : _a, _b = options.start, start = _b === void 0 ? true : _b, _c = options.end, end = _c === void 0 ? true : _c, _d = options.encode, encode = _d === void 0 ? function (x) { return x; } : _d, _e = options.delimiter, delimiter = _e === void 0 ? "/#?" : _e, _f = options.endsWith, endsWith = _f === void 0 ? "" : _f; + var endsWithRe = "[".concat(escapeString(endsWith), "]|$"); + var delimiterRe = "[".concat(escapeString(delimiter), "]"); + var route = start ? "^" : ""; + // Iterate over the tokens and create our regexp string. + for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { + var token = tokens_1[_i]; + if (typeof token === "string") { + route += escapeString(encode(token)); + } + else { + var prefix = escapeString(encode(token.prefix)); + var suffix = escapeString(encode(token.suffix)); + if (token.pattern) { + if (keys) + keys.push(token); + if (prefix || suffix) { + if (token.modifier === "+" || token.modifier === "*") { + var mod = token.modifier === "*" ? "?" : ""; + route += "(?:".concat(prefix, "((?:").concat(token.pattern, ")(?:").concat(suffix).concat(prefix, "(?:").concat(token.pattern, "))*)").concat(suffix, ")").concat(mod); + } + else { + route += "(?:".concat(prefix, "(").concat(token.pattern, ")").concat(suffix, ")").concat(token.modifier); + } + } + else { + if (token.modifier === "+" || token.modifier === "*") { + route += "((?:".concat(token.pattern, ")").concat(token.modifier, ")"); + } + else { + route += "(".concat(token.pattern, ")").concat(token.modifier); + } + } + } + else { + route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier); + } + } + } + if (end) { + if (!strict) + route += "".concat(delimiterRe, "?"); + route += !options.endsWith ? "$" : "(?=".concat(endsWithRe, ")"); + } + else { + var endToken = tokens[tokens.length - 1]; + var isEndDelimited = typeof endToken === "string" + ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 + : endToken === undefined; + if (!strict) { + route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?"); + } + if (!isEndDelimited) { + route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")"); + } + } + 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 }]`. + */ + function pathToRegexp(path, keys, options) { + if (path instanceof RegExp) + return regexpToRegexp(path, keys); + if (Array.isArray(path)) + return arrayToRegexp(path, keys, options); + return stringToRegexp(path, keys, options); + } + + /** + * Cancel token + * @private + * @type { Symbol } + */ + const CANCEL = Symbol(); + + /** + * Helper that can be returned by ruit function to cancel the tasks chain + * @returns { Symbol } internal private constant + * @example + * + * ruit( + * 100, + * num => Math.random() * num + * num => num > 50 ? ruit.cancel() : num + * num => num - 2 + * ).then(result => { + * console.log(result) // here we will get only number lower than 50 + * }) + * + */ + ruit.cancel = () => CANCEL; + + /** + * The same as ruit() but with the arguments inverted from right to left + * @param { * } tasks - list of tasks to process sequentially + * @returns { Promise } a promise containing the result of the whole chain + * @example + * + * const curry = f => a => b => f(a, b) + * const add = (a, b) => a + b + * + * const addOne = curry(add)(1) + * + * const squareAsync = (num) => { + * return new Promise(r => { + * setTimeout(r, 500, num * 2) + * }) + * } + * + * // a -> a + a -> a * 2 + * // basically from right to left: 1 => 1 + 1 => 2 * 2 + * ruit.compose(squareAsync, addOne, 1).then(result => console.log(result)) // 4 + */ + ruit.compose = (...tasks) => ruit(...tasks.reverse()); + + /** + * Serialize a list of sync and async tasks from left to right + * @param { * } tasks - list of tasks to process sequentially + * @returns { Promise } a promise containing the result of the whole chain + * @example + * + * const curry = f => a => b => f(a, b) + * const add = (a, b) => a + b + * + * const addOne = curry(add)(1) + * + * const squareAsync = (num) => { + * return new Promise(r => { + * setTimeout(r, 500, num * 2) + * }) + * } + * + * // a -> a + a -> a * 2 + * // basically from left to right: 1 => 1 + 1 => 2 * 2 + * ruit(1, addOne, squareAsync).then(result => console.log(result)) // 4 + */ + function ruit(...tasks) { + return new Promise((resolve, reject) => { + return (function run(queue, result) { + if (!queue.length) return resolve(result) + + const [task, ...rest] = queue; + const value = typeof task === 'function' ? task(result) : task; + const done = v => run(rest, v); + + // check against nil values + if (value != null) { + if (value === CANCEL) return + if (value.then) return value.then(done, reject) + } + + return Promise.resolve(done(value)) + })(tasks) + }) + } + + // Store the erre the API methods to handle the plugins installation + const API_METHODS = new Set(); + const UNSUBSCRIBE_SYMBOL = Symbol(); + const UNSUBSCRIBE_METHOD = 'off'; + const CANCEL_METHOD = 'cancel'; + + /** + * Factory function to create the stream generator + * @private + * @param {Set} modifiers - stream input modifiers + * @returns {Generator} the stream generator + */ + function createStream(modifiers) { + const stream = (function *stream() { + while (true) { + // get the initial stream value + const input = yield; + + // run the input sequence + yield ruit(input, ...modifiers); + } + })(); + + // start the stream + stream.next(); + + return stream + } + + /** + * Dispatch a value to several listeners + * @private + * @param {Set} callbacks - callbacks collection + * @param {*} value - anything + * @returns {Set} the callbacks received + */ + function dispatch(callbacks, value) { + callbacks.forEach(f => { + // unsubscribe the callback if erre.unsubscribe() will be returned + if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f); + }); + + return callbacks + } + + /** + * Throw a panic error + * @param {string} message - error message + * @returns {Error} an error object + */ + function panic$1(message) { + throw new Error(message) + } + + /** + * Install an erre plugin adding it to the API + * @param {string} name - plugin name + * @param {Function} fn - new erre API method + * @returns {Function} return the erre function + */ + erre.install = function(name, fn) { + if (!name || typeof name !== 'string') + panic$1('Please provide a name (as string) for your erre plugin'); + if (!fn || typeof fn !== 'function') + panic$1('Please provide a function for your erre plugin'); + + if (API_METHODS.has(name)) { + panic$1(`The ${name} is already part of the erre API, please provide a different name`); + } else { + erre[name] = fn; + API_METHODS.add(name); + } + + return erre + }; + + // alias for ruit canel to stop a stream chain + erre.install(CANCEL_METHOD, ruit.cancel); + + // unsubscribe helper + erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL); + + /** + * Stream constuction function + * @param {...Function} fns - stream modifiers + * @returns {Object} erre instance + */ + function erre(...fns) { + const + [success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)], + generator = createStream(modifiers), + stream = Object.create(generator), + addToCollection = (collection) => (fn) => collection.add(fn) && stream, + deleteFromCollection = (collection) => (fn) => collection.delete(fn) ? stream + : panic$1('Couldn\'t remove handler passed by reference'); + + return Object.assign(stream, { + on: Object.freeze({ + value: addToCollection(success), + error: addToCollection(error), + end: addToCollection(end) + }), + off: Object.freeze({ + value: deleteFromCollection(success), + error: deleteFromCollection(error), + end: deleteFromCollection(end) + }), + connect: addToCollection(modifiers), + push(input) { + const { value, done } = stream.next(input); + + // dispatch the stream events + if (!done) { + value.then( + res => dispatch(success, res), + err => dispatch(error, err) + ); + } + + return stream + }, + end() { + // kill the stream + generator.return(); + // dispatch the end event + dispatch(end) + // clean up all the collections + ;[success, error, end, modifiers].forEach(el => el.clear()); + + return stream + }, + fork() { + return erre(...modifiers) + }, + next(input) { + // get the input and run eventually the promise + const result = generator.next(input); + + // pause to the next iteration + generator.next(); + + return result + } + }) + } + + const isString = str => typeof str === 'string'; + const parseURL = (...args) => new URL(...args); + + /** + * Replace the base path from a path + * @param {string} path - router path string + * @returns {string} path cleaned up without the base + */ + const replaceBase = path => path.replace(defaults.base, ''); + + /** + * Try to match the current path or skip it + * @param {RegExp} pathRegExp - target path transformed by pathToRegexp + * @returns {string|Symbol} if the path match we return it otherwise we cancel the stream + */ + const matchOrSkip = pathRegExp => path => match(path, pathRegExp) ? path : erre.cancel(); + + /** + * Combine 2 streams connecting the events of dispatcherStream to the receiverStream + * @param {Stream} dispatcherStream - main stream dispatching events + * @param {Stream} receiverStream - sub stream receiving events from the dispatcher + * @returns {Stream} receiverStream + */ + const joinStreams = (dispatcherStream, receiverStream) => { + dispatcherStream.on.value(receiverStream.push); + + receiverStream.on.end(() => { + dispatcherStream.off.value(receiverStream.push); + }); + + return receiverStream + }; + + /** + * Error handling function + * @param {Error} error - error to catch + * @returns {void} + */ + /* c8 ignore start */ + const panic = error => { + if (defaults.silentErrors) return + + throw new Error(error) + }; + /* c8 ignore stop */ + + // make sure that the router will always receive strings params + const filterStrings = str => isString(str) ? str : erre.cancel(); + + // create the streaming router + const router = erre(filterStrings).on.error(panic); // cast the values of this stream always to string + + /** + * Merge the user options with the defaults + * @param {Object} options - custom user options + * @returns {Object} options object merged with defaults + */ + const mergeOptions = options => ({...defaults, ...options}); + + /* @type {object} general configuration object */ + const defaults = { + base: 'https://localhost', + silentErrors: false, + // pathToRegexp options + sensitive: false, + strict: false, + end: true, + start: true, + delimiter: '/#?', + encode: undefined, + endsWith: undefined, + prefixes: './' + }; + + /** + * Configure the router options overriding the defaults + * @param {Object} options - custom user options to override + * @returns {Object} new defaults + */ + const configure = (options) => { + Object.entries(options).forEach(([key, value]) => { + if (Object.hasOwn(defaults, key)) defaults[key] = value; + }); + + return defaults + }; + + + + /* {@link https://github.com/pillarjs/path-to-regexp#usage} */ + const toRegexp = (path, keys, options) => pathToRegexp(path, keys, mergeOptions(options)); + + /** + * Convert a router entry to a real path computing the url parameters + * @param {string} path - router path string + * @param {Object} params - named matched parameters + * @param {Object} options - pathToRegexp options object + * @returns {string} computed url string + */ + const toPath = (path, params, options) => compile(path, mergeOptions(options))(params); + + /** + * Parse a string path generating an object containing + * @param {string} path - target path + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @param {Object} options - object containing the base path + * @returns {URL} url object enhanced with the `match` attribute + */ + const toURL = (path, pathRegExp, options = {}) => { + const {base} = mergeOptions(options); + const [, ...params] = pathRegExp.exec(path); + const url = parseURL(path, base); + + // extend the url object adding the matched params + url.params = params.reduce((acc, param, index) => { + const key = options.keys && options.keys[index]; + if (key) acc[key.name] = param ? decodeURIComponent(param) : param; + return acc + }, {}); + + return url + }; + + /** + * Return true if a path will be matched + * @param {string} path - target path + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @returns {boolean} true if the path matches the regexp + */ + const match = (path, pathRegExp) => pathRegExp.test(path); + + /** + * Factory function to create an sequence of functions to pass to erre.js + * This function will be used in the erre stream + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @param {Object} options - pathToRegexp options object + * @returns {Array} a functions array that will be used as stream pipe for erre.js + */ + const createURLStreamPipe = (pathRegExp, options) => [ + decodeURI, + replaceBase, + matchOrSkip(pathRegExp), + path => toURL(path, pathRegExp, options) + ]; + + /** + * Create a fork of the main router stream + * @param {string} path - route to match + * @param {Object} options - pathToRegexp options object + * @returns {Stream} new route stream + */ + function createRoute(path, options) { + const keys = []; + const pathRegExp = pathToRegexp(path, keys, options); + const URLStream = erre(...createURLStreamPipe(pathRegExp, { + ...options, + keys + })); + + return joinStreams(router, URLStream).on.error(panic) + } + + var routeHoc = {}; + + var routerHoc = {}; + + const getCurrentRoute = ((currentRoute) => { + // listen the route changes events to store the current route + router.on.value((r) => (currentRoute = r)); + + return () => { + return currentRoute + } + })(null); + + const WINDOW_EVENTS = 'popstate'; + const CLICK_EVENT = 'click'; + const DOWNLOAD_LINK_ATTRIBUTE = 'download'; + const HREF_LINK_ATTRIBUTE = 'href'; + const TARGET_SELF_LINK_ATTRIBUTE = '_self'; + const LINK_TAG_NAME = 'A'; + const HASH = '#'; + const SLASH = '/'; + const RE_ORIGIN = /^.+?\/\/+[^/]+/; + + /** + * Converts any DOM node/s to a loopable array + * @param { HTMLElement|NodeList } els - single html element or a node list + * @returns { Array } always a loopable object + */ + function domToArray(els) { + // can this object be already looped? + if (!Array.isArray(els)) { + // is it a node list? + if ( + /^\[object (HTMLCollection|NodeList|Object)\]$/ + .test(Object.prototype.toString.call(els)) + && typeof els.length === 'number' + ) + return Array.from(els) + else + // if it's a single node + // it will be returned as "array" with one single entry + return [els] + } + // this object could be looped out of the box + return els + } + + /** + * Split a string into several items separed by spaces + * @param { string } l - events list + * @returns { Array } all the events detected + * @private + */ + const split = l => l.split(/\s/); + + /** + * Set a listener for all the events received separated by spaces + * @param { HTMLElement|NodeList|Array } els - DOM node/s where the listeners will be bound + * @param { string } evList - list of events we want to bind or unbind space separated + * @param { Function } cb - listeners callback + * @param { string } method - either 'addEventListener' or 'removeEventListener' + * @param { Object } options - event options (capture, once and passive) + * @returns { undefined } + * @private + */ + function manageEvents(els, evList, cb, method, options) { + els = domToArray(els); + + split(evList).forEach((e) => { + els.forEach(el => el[method](e, cb, options || false)); + }); + } + + /** + * Set a listener for all the events received separated by spaces + * @param { HTMLElement|Array } els - DOM node/s where the listeners will be bound + * @param { string } evList - list of events we want to bind space separated + * @param { Function } cb - listeners callback + * @param { Object } options - event options (capture, once and passive) + * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function + */ + function add(els, evList, cb, options) { + manageEvents(els, evList, cb, 'addEventListener', options); + return els + } + + /** + * Remove all the listeners for the events received separated by spaces + * @param { HTMLElement|Array } els - DOM node/s where the events will be unbind + * @param { string } evList - list of events we want unbind space separated + * @param { Function } cb - listeners callback + * @param { Object } options - event options (capture, once and passive) + * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function + */ + function remove(els, evList, cb, options) { + manageEvents(els, evList, cb, 'removeEventListener', options); + return els + } + + const getGlobal = () => getWindow() || global; + const getWindow = () => (typeof window === 'undefined' ? null : window); + const getDocument = () => + typeof document === 'undefined' ? null : document; + const getHistory = () => + typeof history === 'undefined' ? null : history; + const getLocation = () => { + const win = getWindow(); + return win ? win.location : {} + }; + + (() => { + const globalScope = getGlobal(); + + return globalScope.requestAnimationFrame || globalScope.setTimeout + })(); + + (() => { + const globalScope = getGlobal(); + + return globalScope.cancelAnimationFrame || globalScope.clearTimeout + })(); + + /** + * Normalize the return values, in case of a single value we avoid to return an array + * @param { Array } values - list of values we want to return + * @returns { Array|string|boolean } either the whole list of values or the single one found + * @private + */ + const normalize = values => values.length === 1 ? values[0] : values; + + /** + * Parse all the nodes received to get/remove/check their attributes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes + * @param { string } method - method that will be used to parse the attributes + * @returns { Array|string } result of the parsing in a list or a single value + * @private + */ + function parseNodes(els, name, method) { + const names = typeof name === 'string' ? [name] : name; + return normalize(domToArray(els).map(el => { + return normalize(names.map(n => el[method](n))) + })) + } + + /** + * Set any attribute on a single or a list of DOM nodes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes to detect + * @returns { boolean|Array } true or false or an array of boolean values + * @example + * + * import { has } from 'bianco.attr' + * + * has(img, 'width') // false + * + * // or also + * has(img, ['width', 'height']) // => [false, false] + * + * // or also + * has([img1, img2], ['width', 'height']) // => [[false, false], [false, false]] + */ + function has(els, name) { + return parseNodes(els, name, 'hasAttribute') + } + + const onWindowEvent = () => + router.push(normalizePath(String(getLocation().href))); + const onRouterPush = (path) => { + const url = path.includes(defaults.base) ? path : defaults.base + path; + const loc = getLocation(); + const hist = getHistory(); + const doc = getDocument(); + + // update the browser history only if it's necessary + if (hist && url !== loc.href) { + hist.pushState(null, doc.title, url); + } + }; + const getLinkElement = (node) => + node && !isLinkNode(node) ? getLinkElement(node.parentNode) : node; + const isLinkNode = (node) => node.nodeName === LINK_TAG_NAME; + const isCrossOriginLink = (path) => + path.indexOf(getLocation().href.match(RE_ORIGIN)[0]) === -1; + const isTargetSelfLink = (el) => + el.target && el.target !== TARGET_SELF_LINK_ATTRIBUTE; + const isEventForbidden = (event) => + (event.which && event.which !== 1) || // not left click + event.metaKey || + event.ctrlKey || + event.shiftKey || // or meta keys + event.defaultPrevented; // or default prevented + const isForbiddenLink = (el) => + !el || + !isLinkNode(el) || // not A tag + has(el, DOWNLOAD_LINK_ATTRIBUTE) || // has download attr + !has(el, HREF_LINK_ATTRIBUTE) || // has no href attr + isTargetSelfLink(el) || + isCrossOriginLink(el.href); + const normalizePath = (path) => path.replace(defaults.base, ''); + const isInBase = (path) => !defaults.base || path.includes(defaults.base); + + /** + * Callback called anytime something will be clicked on the page + * @param {Event} event - click event + * @returns {undefined} void method + */ + const onClick = (event) => { + if (isEventForbidden(event)) return + + const el = getLinkElement(event.target); + + if (isForbiddenLink(el) || !isInBase(el.href)) return + + event.preventDefault(); + + router.push(normalizePath(el.href)); + }; + + /** + * Link the rawth router to the DOM events + * @param { HTMLElement } container - DOM node where the links are located + * @returns {Function} teardown function + */ + function initDomListeners(container) { + const win = getWindow(); + const root = container || getDocument(); + + if (win) { + add(win, WINDOW_EVENTS, onWindowEvent); + add(root, CLICK_EVENT, onClick); + } + + router.on.value(onRouterPush); + + return () => { + if (win) { + remove(win, WINDOW_EVENTS, onWindowEvent); + remove(root, CLICK_EVENT, onClick); + } + + router.off.value(onRouterPush); + } + } + + const normalizeInitialSlash = (str) => + str[0] === SLASH ? str : `${SLASH}${str}`; + const removeTrailingSlash = (str) => + str[str.length - 1] === SLASH ? str.substr(0, str.length - 1) : str; + + const normalizeBase = (base) => { + const win = getWindow(); + const loc = win.location; + const root = loc ? `${loc.protocol}//${loc.host}` : ''; + const { pathname } = loc ? loc : {}; + + switch (true) { + // pure root url + pathname + case Boolean(base) === false: + return removeTrailingSlash(`${root}${pathname || ''}`) + // full path base + case /(www|http(s)?:)/.test(base): + return base + // hash navigation + case base[0] === HASH: + return `${root}${pathname && pathname !== SLASH ? pathname : ''}${base}` + // root url with trailing slash + case base === SLASH: + return removeTrailingSlash(root) + // custom pathname + default: + return removeTrailingSlash(`${root}${normalizeInitialSlash(base)}`) + } + }; + + function setBase(base) { + configure({ base: normalizeBase(base) }); + } + + exports.Route = routeHoc; + exports.Router = routerHoc; + exports.configure = configure; + exports.createURLStreamPipe = createURLStreamPipe; + exports.defaults = defaults; + exports.getCurrentRoute = getCurrentRoute; + exports.initDomListeners = initDomListeners; + exports.match = match; + exports.route = createRoute; + exports.router = router; + exports.setBase = setBase; + exports.toPath = toPath; + exports.toRegexp = toRegexp; + exports.toURL = toURL; + +})); diff --git a/node_modules/@riotjs/route/index.umd.js b/node_modules/@riotjs/route/index.umd.js new file mode 100644 index 0000000..9cb9192 --- /dev/null +++ b/node_modules/@riotjs/route/index.umd.js @@ -0,0 +1,1435 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('riot')) : + typeof define === 'function' && define.amd ? define(['exports', 'riot'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.route = {}, global.riot)); +})(this, (function (exports, riot) { 'use strict'; + + /** + * Tokenize input string. + */ + function lexer(str) { + var tokens = []; + var i = 0; + while (i < str.length) { + var char = str[i]; + if (char === "*" || char === "+" || char === "?") { + tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); + continue; + } + if (char === "\\") { + tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); + continue; + } + if (char === "{") { + tokens.push({ type: "OPEN", index: i, value: str[i++] }); + continue; + } + if (char === "}") { + tokens.push({ type: "CLOSE", index: i, value: str[i++] }); + continue; + } + if (char === ":") { + var name = ""; + var j = i + 1; + while (j < str.length) { + var code = str.charCodeAt(j); + if ( + // `0-9` + (code >= 48 && code <= 57) || + // `A-Z` + (code >= 65 && code <= 90) || + // `a-z` + (code >= 97 && code <= 122) || + // `_` + code === 95) { + name += str[j++]; + continue; + } + break; + } + if (!name) + throw new TypeError("Missing parameter name at ".concat(i)); + tokens.push({ type: "NAME", index: i, value: name }); + i = j; + continue; + } + if (char === "(") { + var count = 1; + var pattern = ""; + var j = i + 1; + if (str[j] === "?") { + throw new TypeError("Pattern cannot start with \"?\" at ".concat(j)); + } + while (j < str.length) { + if (str[j] === "\\") { + pattern += str[j++] + str[j++]; + continue; + } + if (str[j] === ")") { + count--; + if (count === 0) { + j++; + break; + } + } + else if (str[j] === "(") { + count++; + if (str[j + 1] !== "?") { + throw new TypeError("Capturing groups are not allowed at ".concat(j)); + } + } + pattern += str[j++]; + } + if (count) + throw new TypeError("Unbalanced pattern at ".concat(i)); + if (!pattern) + throw new TypeError("Missing pattern at ".concat(i)); + tokens.push({ type: "PATTERN", index: i, value: pattern }); + i = j; + continue; + } + tokens.push({ type: "CHAR", index: i, value: str[i++] }); + } + tokens.push({ type: "END", index: i, value: "" }); + return tokens; + } + /** + * Parse a string for the raw tokens. + */ + function parse(str, options) { + if (options === void 0) { options = {}; } + var tokens = lexer(str); + var _a = options.prefixes, prefixes = _a === void 0 ? "./" : _a; + var defaultPattern = "[^".concat(escapeString(options.delimiter || "/#?"), "]+?"); + var result = []; + var key = 0; + var i = 0; + var path = ""; + var tryConsume = function (type) { + if (i < tokens.length && tokens[i].type === type) + return tokens[i++].value; + }; + var mustConsume = function (type) { + var value = tryConsume(type); + if (value !== undefined) + return value; + var _a = tokens[i], nextType = _a.type, index = _a.index; + throw new TypeError("Unexpected ".concat(nextType, " at ").concat(index, ", expected ").concat(type)); + }; + var consumeText = function () { + var result = ""; + var value; + while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { + result += value; + } + return result; + }; + while (i < tokens.length) { + var char = tryConsume("CHAR"); + var name = tryConsume("NAME"); + var pattern = tryConsume("PATTERN"); + if (name || pattern) { + var prefix = char || ""; + if (prefixes.indexOf(prefix) === -1) { + path += prefix; + prefix = ""; + } + if (path) { + result.push(path); + path = ""; + } + result.push({ + name: name || key++, + prefix: prefix, + suffix: "", + pattern: pattern || defaultPattern, + modifier: tryConsume("MODIFIER") || "", + }); + continue; + } + var value = char || tryConsume("ESCAPED_CHAR"); + if (value) { + path += value; + continue; + } + if (path) { + result.push(path); + path = ""; + } + var open = tryConsume("OPEN"); + if (open) { + var prefix = consumeText(); + var name_1 = tryConsume("NAME") || ""; + var pattern_1 = tryConsume("PATTERN") || ""; + var suffix = consumeText(); + mustConsume("CLOSE"); + result.push({ + name: name_1 || (pattern_1 ? key++ : ""), + pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1, + prefix: prefix, + suffix: suffix, + modifier: tryConsume("MODIFIER") || "", + }); + continue; + } + mustConsume("END"); + } + return result; + } + /** + * Compile a string to a template function for the path. + */ + function compile(str, options) { + return tokensToFunction(parse(str, options), options); + } + /** + * Expose a method for transforming tokens into the path function. + */ + function tokensToFunction(tokens, options) { + if (options === void 0) { options = {}; } + var reFlags = flags(options); + var _a = options.encode, encode = _a === void 0 ? function (x) { return x; } : _a, _b = options.validate, validate = _b === void 0 ? true : _b; + // Compile all the tokens into regexps. + var matches = tokens.map(function (token) { + if (typeof token === "object") { + return new RegExp("^(?:".concat(token.pattern, ")$"), reFlags); + } + }); + return function (data) { + var path = ""; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (typeof token === "string") { + path += token; + continue; + } + var value = data ? data[token.name] : undefined; + var optional = token.modifier === "?" || token.modifier === "*"; + var repeat = token.modifier === "*" || token.modifier === "+"; + if (Array.isArray(value)) { + if (!repeat) { + throw new TypeError("Expected \"".concat(token.name, "\" to not repeat, but got an array")); + } + if (value.length === 0) { + if (optional) + continue; + throw new TypeError("Expected \"".concat(token.name, "\" to not be empty")); + } + for (var j = 0; j < value.length; j++) { + var segment = encode(value[j], token); + if (validate && !matches[i].test(segment)) { + throw new TypeError("Expected all \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\"")); + } + path += token.prefix + segment + token.suffix; + } + continue; + } + if (typeof value === "string" || typeof value === "number") { + var segment = encode(String(value), token); + if (validate && !matches[i].test(segment)) { + throw new TypeError("Expected \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\"")); + } + path += token.prefix + segment + token.suffix; + continue; + } + if (optional) + continue; + var typeOfMessage = repeat ? "an array" : "a string"; + throw new TypeError("Expected \"".concat(token.name, "\" to be ").concat(typeOfMessage)); + } + return path; + }; + } + /** + * Escape a regular expression string. + */ + function escapeString(str) { + return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); + } + /** + * Get the flags for a regexp from the options. + */ + function flags(options) { + return options && options.sensitive ? "" : "i"; + } + /** + * Pull out keys from a regexp. + */ + function regexpToRegexp(path, keys) { + if (!keys) + return path; + var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; + var index = 0; + var execResult = groupsRegex.exec(path.source); + while (execResult) { + keys.push({ + // Use parenthesized substring match if available, index otherwise + name: execResult[1] || index++, + prefix: "", + suffix: "", + modifier: "", + pattern: "", + }); + execResult = groupsRegex.exec(path.source); + } + return path; + } + /** + * Transform an array into a regexp. + */ + function arrayToRegexp(paths, keys, options) { + var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source; }); + return new RegExp("(?:".concat(parts.join("|"), ")"), flags(options)); + } + /** + * Create a path regexp from string input. + */ + function stringToRegexp(path, keys, options) { + return tokensToRegexp(parse(path, options), keys, options); + } + /** + * Expose a function for taking tokens and returning a RegExp. + */ + function tokensToRegexp(tokens, keys, options) { + if (options === void 0) { options = {}; } + var _a = options.strict, strict = _a === void 0 ? false : _a, _b = options.start, start = _b === void 0 ? true : _b, _c = options.end, end = _c === void 0 ? true : _c, _d = options.encode, encode = _d === void 0 ? function (x) { return x; } : _d, _e = options.delimiter, delimiter = _e === void 0 ? "/#?" : _e, _f = options.endsWith, endsWith = _f === void 0 ? "" : _f; + var endsWithRe = "[".concat(escapeString(endsWith), "]|$"); + var delimiterRe = "[".concat(escapeString(delimiter), "]"); + var route = start ? "^" : ""; + // Iterate over the tokens and create our regexp string. + for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { + var token = tokens_1[_i]; + if (typeof token === "string") { + route += escapeString(encode(token)); + } + else { + var prefix = escapeString(encode(token.prefix)); + var suffix = escapeString(encode(token.suffix)); + if (token.pattern) { + if (keys) + keys.push(token); + if (prefix || suffix) { + if (token.modifier === "+" || token.modifier === "*") { + var mod = token.modifier === "*" ? "?" : ""; + route += "(?:".concat(prefix, "((?:").concat(token.pattern, ")(?:").concat(suffix).concat(prefix, "(?:").concat(token.pattern, "))*)").concat(suffix, ")").concat(mod); + } + else { + route += "(?:".concat(prefix, "(").concat(token.pattern, ")").concat(suffix, ")").concat(token.modifier); + } + } + else { + if (token.modifier === "+" || token.modifier === "*") { + route += "((?:".concat(token.pattern, ")").concat(token.modifier, ")"); + } + else { + route += "(".concat(token.pattern, ")").concat(token.modifier); + } + } + } + else { + route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier); + } + } + } + if (end) { + if (!strict) + route += "".concat(delimiterRe, "?"); + route += !options.endsWith ? "$" : "(?=".concat(endsWithRe, ")"); + } + else { + var endToken = tokens[tokens.length - 1]; + var isEndDelimited = typeof endToken === "string" + ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 + : endToken === undefined; + if (!strict) { + route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?"); + } + if (!isEndDelimited) { + route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")"); + } + } + 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 }]`. + */ + function pathToRegexp(path, keys, options) { + if (path instanceof RegExp) + return regexpToRegexp(path, keys); + if (Array.isArray(path)) + return arrayToRegexp(path, keys, options); + return stringToRegexp(path, keys, options); + } + + /** + * Cancel token + * @private + * @type { Symbol } + */ + const CANCEL = Symbol(); + + /** + * Helper that can be returned by ruit function to cancel the tasks chain + * @returns { Symbol } internal private constant + * @example + * + * ruit( + * 100, + * num => Math.random() * num + * num => num > 50 ? ruit.cancel() : num + * num => num - 2 + * ).then(result => { + * console.log(result) // here we will get only number lower than 50 + * }) + * + */ + ruit.cancel = () => CANCEL; + + /** + * The same as ruit() but with the arguments inverted from right to left + * @param { * } tasks - list of tasks to process sequentially + * @returns { Promise } a promise containing the result of the whole chain + * @example + * + * const curry = f => a => b => f(a, b) + * const add = (a, b) => a + b + * + * const addOne = curry(add)(1) + * + * const squareAsync = (num) => { + * return new Promise(r => { + * setTimeout(r, 500, num * 2) + * }) + * } + * + * // a -> a + a -> a * 2 + * // basically from right to left: 1 => 1 + 1 => 2 * 2 + * ruit.compose(squareAsync, addOne, 1).then(result => console.log(result)) // 4 + */ + ruit.compose = (...tasks) => ruit(...tasks.reverse()); + + /** + * Serialize a list of sync and async tasks from left to right + * @param { * } tasks - list of tasks to process sequentially + * @returns { Promise } a promise containing the result of the whole chain + * @example + * + * const curry = f => a => b => f(a, b) + * const add = (a, b) => a + b + * + * const addOne = curry(add)(1) + * + * const squareAsync = (num) => { + * return new Promise(r => { + * setTimeout(r, 500, num * 2) + * }) + * } + * + * // a -> a + a -> a * 2 + * // basically from left to right: 1 => 1 + 1 => 2 * 2 + * ruit(1, addOne, squareAsync).then(result => console.log(result)) // 4 + */ + function ruit(...tasks) { + return new Promise((resolve, reject) => { + return (function run(queue, result) { + if (!queue.length) return resolve(result) + + const [task, ...rest] = queue; + const value = typeof task === 'function' ? task(result) : task; + const done = v => run(rest, v); + + // check against nil values + if (value != null) { + if (value === CANCEL) return + if (value.then) return value.then(done, reject) + } + + return Promise.resolve(done(value)) + })(tasks) + }) + } + + // Store the erre the API methods to handle the plugins installation + const API_METHODS = new Set(); + const UNSUBSCRIBE_SYMBOL = Symbol(); + const UNSUBSCRIBE_METHOD = 'off'; + const CANCEL_METHOD = 'cancel'; + + /** + * Factory function to create the stream generator + * @private + * @param {Set} modifiers - stream input modifiers + * @returns {Generator} the stream generator + */ + function createStream(modifiers) { + const stream = (function *stream() { + while (true) { + // get the initial stream value + const input = yield; + + // run the input sequence + yield ruit(input, ...modifiers); + } + })(); + + // start the stream + stream.next(); + + return stream + } + + /** + * Dispatch a value to several listeners + * @private + * @param {Set} callbacks - callbacks collection + * @param {*} value - anything + * @returns {Set} the callbacks received + */ + function dispatch(callbacks, value) { + callbacks.forEach(f => { + // unsubscribe the callback if erre.unsubscribe() will be returned + if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f); + }); + + return callbacks + } + + /** + * Throw a panic error + * @param {string} message - error message + * @returns {Error} an error object + */ + function panic$1(message) { + throw new Error(message) + } + + /** + * Install an erre plugin adding it to the API + * @param {string} name - plugin name + * @param {Function} fn - new erre API method + * @returns {Function} return the erre function + */ + erre.install = function(name, fn) { + if (!name || typeof name !== 'string') + panic$1('Please provide a name (as string) for your erre plugin'); + if (!fn || typeof fn !== 'function') + panic$1('Please provide a function for your erre plugin'); + + if (API_METHODS.has(name)) { + panic$1(`The ${name} is already part of the erre API, please provide a different name`); + } else { + erre[name] = fn; + API_METHODS.add(name); + } + + return erre + }; + + // alias for ruit canel to stop a stream chain + erre.install(CANCEL_METHOD, ruit.cancel); + + // unsubscribe helper + erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL); + + /** + * Stream constuction function + * @param {...Function} fns - stream modifiers + * @returns {Object} erre instance + */ + function erre(...fns) { + const + [success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)], + generator = createStream(modifiers), + stream = Object.create(generator), + addToCollection = (collection) => (fn) => collection.add(fn) && stream, + deleteFromCollection = (collection) => (fn) => collection.delete(fn) ? stream + : panic$1('Couldn\'t remove handler passed by reference'); + + return Object.assign(stream, { + on: Object.freeze({ + value: addToCollection(success), + error: addToCollection(error), + end: addToCollection(end) + }), + off: Object.freeze({ + value: deleteFromCollection(success), + error: deleteFromCollection(error), + end: deleteFromCollection(end) + }), + connect: addToCollection(modifiers), + push(input) { + const { value, done } = stream.next(input); + + // dispatch the stream events + if (!done) { + value.then( + res => dispatch(success, res), + err => dispatch(error, err) + ); + } + + return stream + }, + end() { + // kill the stream + generator.return(); + // dispatch the end event + dispatch(end) + // clean up all the collections + ;[success, error, end, modifiers].forEach(el => el.clear()); + + return stream + }, + fork() { + return erre(...modifiers) + }, + next(input) { + // get the input and run eventually the promise + const result = generator.next(input); + + // pause to the next iteration + generator.next(); + + return result + } + }) + } + + const isString = str => typeof str === 'string'; + const parseURL = (...args) => new URL(...args); + + /** + * Replace the base path from a path + * @param {string} path - router path string + * @returns {string} path cleaned up without the base + */ + const replaceBase = path => path.replace(defaults.base, ''); + + /** + * Try to match the current path or skip it + * @param {RegExp} pathRegExp - target path transformed by pathToRegexp + * @returns {string|Symbol} if the path match we return it otherwise we cancel the stream + */ + const matchOrSkip = pathRegExp => path => match(path, pathRegExp) ? path : erre.cancel(); + + /** + * Combine 2 streams connecting the events of dispatcherStream to the receiverStream + * @param {Stream} dispatcherStream - main stream dispatching events + * @param {Stream} receiverStream - sub stream receiving events from the dispatcher + * @returns {Stream} receiverStream + */ + const joinStreams = (dispatcherStream, receiverStream) => { + dispatcherStream.on.value(receiverStream.push); + + receiverStream.on.end(() => { + dispatcherStream.off.value(receiverStream.push); + }); + + return receiverStream + }; + + /** + * Error handling function + * @param {Error} error - error to catch + * @returns {void} + */ + /* c8 ignore start */ + const panic$2 = error => { + if (defaults.silentErrors) return + + throw new Error(error) + }; + /* c8 ignore stop */ + + // make sure that the router will always receive strings params + const filterStrings = str => isString(str) ? str : erre.cancel(); + + // create the streaming router + const router = erre(filterStrings).on.error(panic$2); // cast the values of this stream always to string + + /** + * Merge the user options with the defaults + * @param {Object} options - custom user options + * @returns {Object} options object merged with defaults + */ + const mergeOptions = options => ({...defaults, ...options}); + + /* @type {object} general configuration object */ + const defaults = { + base: 'https://localhost', + silentErrors: false, + // pathToRegexp options + sensitive: false, + strict: false, + end: true, + start: true, + delimiter: '/#?', + encode: undefined, + endsWith: undefined, + prefixes: './' + }; + + /** + * Configure the router options overriding the defaults + * @param {Object} options - custom user options to override + * @returns {Object} new defaults + */ + const configure = (options) => { + Object.entries(options).forEach(([key, value]) => { + if (Object.hasOwn(defaults, key)) defaults[key] = value; + }); + + return defaults + }; + + + + /* {@link https://github.com/pillarjs/path-to-regexp#usage} */ + const toRegexp = (path, keys, options) => pathToRegexp(path, keys, mergeOptions(options)); + + /** + * Convert a router entry to a real path computing the url parameters + * @param {string} path - router path string + * @param {Object} params - named matched parameters + * @param {Object} options - pathToRegexp options object + * @returns {string} computed url string + */ + const toPath = (path, params, options) => compile(path, mergeOptions(options))(params); + + /** + * Parse a string path generating an object containing + * @param {string} path - target path + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @param {Object} options - object containing the base path + * @returns {URL} url object enhanced with the `match` attribute + */ + const toURL = (path, pathRegExp, options = {}) => { + const {base} = mergeOptions(options); + const [, ...params] = pathRegExp.exec(path); + const url = parseURL(path, base); + + // extend the url object adding the matched params + url.params = params.reduce((acc, param, index) => { + const key = options.keys && options.keys[index]; + if (key) acc[key.name] = param ? decodeURIComponent(param) : param; + return acc + }, {}); + + return url + }; + + /** + * Return true if a path will be matched + * @param {string} path - target path + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @returns {boolean} true if the path matches the regexp + */ + const match = (path, pathRegExp) => pathRegExp.test(path); + + /** + * Factory function to create an sequence of functions to pass to erre.js + * This function will be used in the erre stream + * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp + * @param {Object} options - pathToRegexp options object + * @returns {Array} a functions array that will be used as stream pipe for erre.js + */ + const createURLStreamPipe = (pathRegExp, options) => [ + decodeURI, + replaceBase, + matchOrSkip(pathRegExp), + path => toURL(path, pathRegExp, options) + ]; + + /** + * Create a fork of the main router stream + * @param {string} path - route to match + * @param {Object} options - pathToRegexp options object + * @returns {Stream} new route stream + */ + function createRoute(path, options) { + const keys = []; + const pathRegExp = pathToRegexp(path, keys, options); + const URLStream = erre(...createURLStreamPipe(pathRegExp, { + ...options, + keys + })); + + return joinStreams(router, URLStream).on.error(panic$2) + } + + const WINDOW_EVENTS = 'popstate'; + const CLICK_EVENT = 'click'; + const DOWNLOAD_LINK_ATTRIBUTE = 'download'; + const HREF_LINK_ATTRIBUTE = 'href'; + const TARGET_SELF_LINK_ATTRIBUTE = '_self'; + const LINK_TAG_NAME = 'A'; + const HASH = '#'; + const SLASH = '/'; + const PATH_ATTRIBUTE = 'path'; + const RE_ORIGIN = /^.+?\/\/+[^/]+/; + + /** + * Converts any DOM node/s to a loopable array + * @param { HTMLElement|NodeList } els - single html element or a node list + * @returns { Array } always a loopable object + */ + function domToArray(els) { + // can this object be already looped? + if (!Array.isArray(els)) { + // is it a node list? + if ( + /^\[object (HTMLCollection|NodeList|Object)\]$/ + .test(Object.prototype.toString.call(els)) + && typeof els.length === 'number' + ) + return Array.from(els) + else + // if it's a single node + // it will be returned as "array" with one single entry + return [els] + } + // this object could be looped out of the box + return els + } + + /** + * Simple helper to find DOM nodes returning them as array like loopable object + * @param { string|DOMNodeList } selector - either the query or the DOM nodes to arraify + * @param { HTMLElement } scope - context defining where the query will search for the DOM nodes + * @returns { Array } DOM nodes found as array + */ + function $(selector, scope) { + return domToArray(typeof selector === 'string' ? + (document).querySelectorAll(selector) : + selector + ) + } + + const getCurrentRoute = ((currentRoute) => { + // listen the route changes events to store the current route + router.on.value((r) => (currentRoute = r)); + + return () => { + return currentRoute + } + })(null); + + /** + * Normalize the return values, in case of a single value we avoid to return an array + * @param { Array } values - list of values we want to return + * @returns { Array|string|boolean } either the whole list of values or the single one found + * @private + */ + const normalize = values => values.length === 1 ? values[0] : values; + + /** + * Parse all the nodes received to get/remove/check their attributes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes + * @param { string } method - method that will be used to parse the attributes + * @returns { Array|string } result of the parsing in a list or a single value + * @private + */ + function parseNodes(els, name, method) { + const names = typeof name === 'string' ? [name] : name; + return normalize(domToArray(els).map(el => { + return normalize(names.map(n => el[method](n))) + })) + } + + /** + * Get any attribute from a single or a list of DOM nodes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes to get + * @returns { Array|string } list of the attributes found + * + * @example + * + * import { get } from 'bianco.attr' + * + * const img = document.createElement('img') + * + * get(img, 'width') // => '200' + * + * // or also + * get(img, ['width', 'height']) // => ['200', '300'] + * + * // or also + * get([img1, img2], ['width', 'height']) // => [['200', '300'], ['500', '200']] + */ + function get(els, name) { + return parseNodes(els, name, 'getAttribute') + } + + /** + * Set any attribute on a single or a list of DOM nodes + * @param { HTMLElement|NodeList|Array } els - DOM node/s to parse + * @param { string|Array } name - name or list of attributes to detect + * @returns { boolean|Array } true or false or an array of boolean values + * @example + * + * import { has } from 'bianco.attr' + * + * has(img, 'width') // false + * + * // or also + * has(img, ['width', 'height']) // => [false, false] + * + * // or also + * has([img1, img2], ['width', 'height']) // => [[false, false], [false, false]] + */ + function has(els, name) { + return parseNodes(els, name, 'hasAttribute') + } + + /** + * Convert a string from camel case to dash-case + * @param {string} string - probably a component tag name + * @returns {string} component name normalized + */ + + /** + * Convert a string containing dashes to camel case + * @param {string} string - input string + * @returns {string} my-string -> myString + */ + function dashToCamelCase(string) { + return string.replace(/-(\w)/g, (_, c) => c.toUpperCase()) + } + + /** + * Check if a value is null or undefined + * @param {*} value - anything + * @returns {boolean} true only for the 'undefined' and 'null' types + */ + function isNil(value) { + return value === null || value === undefined + } + + const getGlobal = () => getWindow() || global; + const getWindow = () => (typeof window === 'undefined' ? null : window); + const getDocument = () => + typeof document === 'undefined' ? null : document; + const getHistory = () => + typeof history === 'undefined' ? null : history; + const getLocation = () => { + const win = getWindow(); + return win ? win.location : {} + }; + + const defer = (() => { + const globalScope = getGlobal(); + + return globalScope.requestAnimationFrame || globalScope.setTimeout + })(); + + const cancelDefer = (() => { + const globalScope = getGlobal(); + + return globalScope.cancelAnimationFrame || globalScope.clearTimeout + })(); + + const getAttribute = (attributes, name, context) => { + if (!attributes) return null + + const normalizedAttributes = attributes.flatMap((attr) => + isNil(attr.name) + ? // add support for spread attributes https://github.com/riot/route/issues/178 + Object.entries(attr.evaluate(context)).map(([key, value]) => ({ + // evaluate each value of the spread attribute and store it into the array + name: key, + // create a nested evaluate function pointing to the original value of the spread object + evaluate: () => value, + })) + : attr, + ); + + return normalizedAttributes.find((a) => dashToCamelCase(a.name) === name) + }; + + const createDefaultSlot = (attributes = []) => { + const { template, bindingTypes, expressionTypes } = riot.__.DOMBindings; + + return template(null, [ + { + type: bindingTypes.SLOT, + name: 'default', + attributes: attributes.map((attr) => ({ + ...attr, + type: expressionTypes.ATTRIBUTE, + })), + }, + ]) + }; + + // True if the selector string is valid + const isValidQuerySelectorString = (selector) => + /^([a-zA-Z0-9-_*#.:[\]\s>+~()='"]|\\.)+$/.test(selector); + + /** + * Similar to compose but performs from left-to-right function composition.
+ * {@link https://30secondsofcode.org/function#composeright see also} + * @param {...[function]} fns) - list of unary function + * @returns {*} result of the computation + */ + + /** + * Performs right-to-left function composition.
+ * Use Array.prototype.reduce() to perform right-to-left function composition.
+ * The last (rightmost) function can accept one or more arguments; the remaining functions must be unary.
+ * {@link https://30secondsofcode.org/function#compose original source code} + * @param {...[function]} fns) - list of unary function + * @returns {*} result of the computation + */ + function compose(...fns) { + return fns.reduce((f, g) => (...args) => f(g(...args))) + } + + const getInitialRouteValue = (pathToRegexp, path, options) => { + const route = compose( + ...createURLStreamPipe(pathToRegexp, options).reverse(), + )(path); + + return route.params ? route : null + }; + + const clearDOMBetweenNodes = (first, last, includeBoundaries) => { + const clear = (node) => { + if (!node || (node === last && !includeBoundaries)) return + const { nextSibling } = node; + node.remove(); + clear(nextSibling); + }; + + clear(includeBoundaries ? first : first.nextSibling); + }; + + const routeHoc$1 = ({ slots, attributes }) => { + const placeholders = { + before: document.createTextNode(''), + after: document.createTextNode(''), + }; + + return { + mount(el, context) { + // create the component state + const currentRoute = getCurrentRoute(); + const path = + getAttribute(attributes, PATH_ATTRIBUTE, context)?.evaluate(context) || + get(el, PATH_ATTRIBUTE); + const pathToRegexp = toRegexp(path, []); + const state = { + pathToRegexp, + route: + currentRoute && match(currentRoute, pathToRegexp) + ? getInitialRouteValue(pathToRegexp, currentRoute, {}) + : null, + }; + this.el = el; + this.slot = createDefaultSlot([ + { + isBoolean: false, + name: 'route', + evaluate: () => this.state.route, + }, + ]); + this.context = context; + this.state = state; + // set the route listeners + this.boundOnBeforeRoute = this.onBeforeRoute.bind(this); + this.boundOnRoute = this.onRoute.bind(this); + router.on.value(this.boundOnBeforeRoute); + this.stream = createRoute(path).on.value(this.boundOnRoute); + // update the DOM + el.replaceWith(placeholders.before); + placeholders.before.parentNode.insertBefore( + placeholders.after, + placeholders.before.nextSibling, + ); + if (state.route) this.mountSlot(); + }, + update(context) { + this.context = context; + if (this.state.route) this.slot.update({}, context); + }, + mountSlot() { + const { route } = this.state; + // insert the route root element after the before placeholder + placeholders.before.parentNode.insertBefore( + this.el, + placeholders.before.nextSibling, + ); + this.callLifecycleProperty('onBeforeMount', route); + this.slot.mount( + this.el, + { + slots, + }, + this.context, + ); + this.callLifecycleProperty('onMounted', route); + }, + clearDOM(includeBoundaries) { + // remove all the DOM nodes between the placeholders + clearDOMBetweenNodes( + placeholders.before, + placeholders.after, + includeBoundaries, + ); + }, + unmount() { + router.off.value(this.boundOnBeforeRoute); + this.slot.unmount({}, this.context, true); + this.clearDOM(true); + this.stream.end(); + }, + onBeforeRoute(path) { + const { route } = this.state; + // this component was not mounted or the current path matches + // we don't need to unmount this component + if (!route || match(path, this.state.pathToRegexp)) return + + this.callLifecycleProperty('onBeforeUnmount', route); + this.slot.unmount({}, this.context, true); + this.clearDOM(false); + this.state.route = null; + this.callLifecycleProperty('onUnmounted', route); + }, + onRoute(route) { + const prevRoute = this.state.route; + this.state.route = route; + + // if this route component was already mounted we need to update it + if (prevRoute) { + this.callLifecycleProperty('onBeforeUpdate', route); + this.slot.update({}, this.context); + this.callLifecycleProperty('onUpdated', route); + } + // this route component was never mounted, so we need to create its DOM + else this.mountSlot(); + + // emulate the default browser anchor links behaviour + if (route.hash && isValidQuerySelectorString(route.hash)) + $(route.hash)?.[0].scrollIntoView(); + }, + callLifecycleProperty(method, ...params) { + const attr = getAttribute(attributes, method, this.context); + + if (attr) attr.evaluate(this.context)(...params); + }, + } + }; + + var routeHoc = { + css: null, + + exports: riot.pure( + routeHoc$1 + ), + + template: null, + name: 'route-hoc' + }; + + const normalizeInitialSlash = (str) => + str[0] === SLASH ? str : `${SLASH}${str}`; + const removeTrailingSlash = (str) => + str[str.length - 1] === SLASH ? str.substr(0, str.length - 1) : str; + + const normalizeBase = (base) => { + const win = getWindow(); + const loc = win.location; + const root = loc ? `${loc.protocol}//${loc.host}` : ''; + const { pathname } = loc ? loc : {}; + + switch (true) { + // pure root url + pathname + case Boolean(base) === false: + return removeTrailingSlash(`${root}${pathname || ''}`) + // full path base + case /(www|http(s)?:)/.test(base): + return base + // hash navigation + case base[0] === HASH: + return `${root}${pathname && pathname !== SLASH ? pathname : ''}${base}` + // root url with trailing slash + case base === SLASH: + return removeTrailingSlash(root) + // custom pathname + default: + return removeTrailingSlash(`${root}${normalizeInitialSlash(base)}`) + } + }; + + function setBase(base) { + configure({ base: normalizeBase(base) }); + } + + /** + * Throw an error with a descriptive message + * @param { string } message - error message + * @param { string } cause - optional error cause object + * @returns { undefined } hoppla... at this point the program should stop working + */ + function panic(message, cause) { + throw new Error(message, { cause }) + } + + /** + * Split a string into several items separed by spaces + * @param { string } l - events list + * @returns { Array } all the events detected + * @private + */ + const split = l => l.split(/\s/); + + /** + * Set a listener for all the events received separated by spaces + * @param { HTMLElement|NodeList|Array } els - DOM node/s where the listeners will be bound + * @param { string } evList - list of events we want to bind or unbind space separated + * @param { Function } cb - listeners callback + * @param { string } method - either 'addEventListener' or 'removeEventListener' + * @param { Object } options - event options (capture, once and passive) + * @returns { undefined } + * @private + */ + function manageEvents(els, evList, cb, method, options) { + els = domToArray(els); + + split(evList).forEach((e) => { + els.forEach(el => el[method](e, cb, options || false)); + }); + } + + /** + * Set a listener for all the events received separated by spaces + * @param { HTMLElement|Array } els - DOM node/s where the listeners will be bound + * @param { string } evList - list of events we want to bind space separated + * @param { Function } cb - listeners callback + * @param { Object } options - event options (capture, once and passive) + * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function + */ + function add(els, evList, cb, options) { + manageEvents(els, evList, cb, 'addEventListener', options); + return els + } + + /** + * Remove all the listeners for the events received separated by spaces + * @param { HTMLElement|Array } els - DOM node/s where the events will be unbind + * @param { string } evList - list of events we want unbind space separated + * @param { Function } cb - listeners callback + * @param { Object } options - event options (capture, once and passive) + * @returns { HTMLElement|NodeList|Array } DOM node/s and first argument of the function + */ + function remove(els, evList, cb, options) { + manageEvents(els, evList, cb, 'removeEventListener', options); + return els + } + + const onWindowEvent = () => + router.push(normalizePath(String(getLocation().href))); + const onRouterPush = (path) => { + const url = path.includes(defaults.base) ? path : defaults.base + path; + const loc = getLocation(); + const hist = getHistory(); + const doc = getDocument(); + + // update the browser history only if it's necessary + if (hist && url !== loc.href) { + hist.pushState(null, doc.title, url); + } + }; + const getLinkElement = (node) => + node && !isLinkNode(node) ? getLinkElement(node.parentNode) : node; + const isLinkNode = (node) => node.nodeName === LINK_TAG_NAME; + const isCrossOriginLink = (path) => + path.indexOf(getLocation().href.match(RE_ORIGIN)[0]) === -1; + const isTargetSelfLink = (el) => + el.target && el.target !== TARGET_SELF_LINK_ATTRIBUTE; + const isEventForbidden = (event) => + (event.which && event.which !== 1) || // not left click + event.metaKey || + event.ctrlKey || + event.shiftKey || // or meta keys + event.defaultPrevented; // or default prevented + const isForbiddenLink = (el) => + !el || + !isLinkNode(el) || // not A tag + has(el, DOWNLOAD_LINK_ATTRIBUTE) || // has download attr + !has(el, HREF_LINK_ATTRIBUTE) || // has no href attr + isTargetSelfLink(el) || + isCrossOriginLink(el.href); + const normalizePath = (path) => path.replace(defaults.base, ''); + const isInBase = (path) => !defaults.base || path.includes(defaults.base); + + /** + * Callback called anytime something will be clicked on the page + * @param {Event} event - click event + * @returns {undefined} void method + */ + const onClick = (event) => { + if (isEventForbidden(event)) return + + const el = getLinkElement(event.target); + + if (isForbiddenLink(el) || !isInBase(el.href)) return + + event.preventDefault(); + + router.push(normalizePath(el.href)); + }; + + /** + * Link the rawth router to the DOM events + * @param { HTMLElement } container - DOM node where the links are located + * @returns {Function} teardown function + */ + function initDomListeners(container) { + const win = getWindow(); + const root = container || getDocument(); + + if (win) { + add(win, WINDOW_EVENTS, onWindowEvent); + add(root, CLICK_EVENT, onClick); + } + + router.on.value(onRouterPush); + + return () => { + if (win) { + remove(win, WINDOW_EVENTS, onWindowEvent); + remove(root, CLICK_EVENT, onClick); + } + + router.off.value(onRouterPush); + } + } + + const BASE_ATTRIBUTE_NAME = 'base'; + const INITIAL_ROUTE = 'initialRoute'; + const ON_STARTED_ATTRIBUTE_NAME = 'onStarted'; + + const routerHoc$1 = ({ slots, attributes, props }) => { + if (routerHoc$1.wasInitialized) + panic('Multiple components are not supported'); + + return { + slot: null, + el: null, + teardown: null, + mount(el, context) { + const initialRouteAttr = getAttribute(attributes, INITIAL_ROUTE, context); + const initialRoute = initialRouteAttr + ? initialRouteAttr.evaluate(context) + : null; + const currentRoute = getCurrentRoute(); + const onFirstRoute = () => { + this.createSlot(context); + router.off.value(onFirstRoute); + }; + routerHoc$1.wasInitialized = true; + + this.el = el; + this.teardown = initDomListeners(this.root); + + this.setBase(context); + + // mount the slots only if the current route was defined + if (currentRoute && !initialRoute) { + this.createSlot(context); + } else { + router.on.value(onFirstRoute); + router.push(initialRoute || window.location.href); + } + }, + createSlot(context) { + if (!slots || !slots.length) return + const onStartedAttr = getAttribute( + attributes, + ON_STARTED_ATTRIBUTE_NAME, + context, + ); + + this.slot = createDefaultSlot(); + + this.slot.mount( + this.el, + { + slots, + }, + context, + ); + + if (onStartedAttr) { + onStartedAttr.evaluate(context)(getCurrentRoute()); + } + }, + update(context) { + this.setBase(context); + + // defer the updates to avoid internal recursive update calls + // see https://github.com/riot/route/issues/148 + if (this.slot) { + cancelDefer(this.deferred); + + this.deferred = defer(() => { + this.slot.update({}, context); + }); + } + }, + unmount(...args) { + this.teardown(); + routerHoc$1.wasInitialized = false; + + if (this.slot) { + this.slot.unmount(...args); + } + }, + getBase(context) { + const baseAttr = getAttribute(attributes, BASE_ATTRIBUTE_NAME, context); + + return baseAttr + ? baseAttr.evaluate(context) + : this.el.getAttribute(BASE_ATTRIBUTE_NAME) || '/' + }, + setBase(context) { + setBase(props ? props.base : this.getBase(context)); + }, + } + }; + + // flag to avoid multiple router instances + routerHoc$1.wasInitialized = false; + + var routerHoc = { + css: null, + + exports: riot.pure( + routerHoc$1 + ), + + template: null, + name: 'router-hoc' + }; + + exports.Route = routeHoc; + exports.Router = routerHoc; + exports.configure = configure; + exports.createURLStreamPipe = createURLStreamPipe; + exports.defaults = defaults; + exports.getCurrentRoute = getCurrentRoute; + exports.initDomListeners = initDomListeners; + exports.match = match; + exports.route = createRoute; + exports.router = router; + exports.setBase = setBase; + exports.toPath = toPath; + exports.toRegexp = toRegexp; + exports.toURL = toURL; + +})); diff --git a/node_modules/@riotjs/route/package.json b/node_modules/@riotjs/route/package.json new file mode 100644 index 0000000..cf8890a --- /dev/null +++ b/node_modules/@riotjs/route/package.json @@ -0,0 +1,92 @@ +{ + "name": "@riotjs/route", + "version": "10.0.0", + "description": "Riot.js isomorphic router", + "type": "module", + "main": "index.umd.js", + "jsnext:main": "index.js", + "module": "index.js", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "require": "./index.umd.js", + "browser": "./index.umd.js" + }, + "./standalone": { + "types": "./index.d.ts", + "import": "./index.standalone.js", + "require": "./index.standalone.umd.js", + "browser": "./index.standalone.umd.js" + } + }, + "scripts": { + "prepublishOnly": "npm run build && npm test", + "lint": "eslint src test rollup.config.js && prettier -c .", + "build": "rollup -c && npm run build-demo", + "build-demo": "riot demos/components -o demos/components", + "demo": "npm run build && serve", + "cov": "c8 report --reporter=lcov", + "cov-html": "c8 report --reporter=html", + "test": "npm run lint && c8 mocha -r test/setup.js test/*.spec.js" + }, + "files": [ + "index.d.ts", + "index.js", + "index.umd.js", + "index.standalone.js", + "index.standalone.umd.js" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/riot/route.git" + }, + "keywords": [ + "riot", + "Riot.js", + "router", + "riot-route", + "route" + ], + "author": "Gianluca Guarini (https://gianlucaguarini.com)", + "license": "MIT", + "bugs": { + "url": "https://github.com/riot/route/issues" + }, + "homepage": "https://github.com/riot/route#readme", + "devDependencies": { + "@riotjs/cli": "10.0.0", + "@riotjs/compiler": "10.0.0", + "@riotjs/prettier-config": "^1.1.0", + "@riotjs/register": "10.0.0", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-virtual": "^3.0.2", + "c8": "^10.1.3", + "chai": "^6.0.1", + "eslint": "^9.33.0", + "eslint-config-riot": "^5.0.2", + "globals": "^16.3.0", + "jsdom": "26.1.0", + "jsdom-global": "3.0.2", + "mocha": "^11.7.1", + "prettier": "^3.6.2", + "riot": "^10.0.0", + "rollup": "^4.47.1", + "rollup-plugin-riot": "10.0.0", + "serve": "^14.2.4", + "sinon": "^21.0.0", + "sinon-chai": "^4.0.1" + }, + "peerDependency": { + "riot": "^6.0.0 || ^7.0.0 || ^9.0.0 || ^10.0.0" + }, + "dependencies": { + "@riotjs/util": "^10.0.0", + "bianco.attr": "^1.1.1", + "bianco.events": "^1.1.1", + "bianco.query": "^1.1.4", + "cumpa": "^2.0.1", + "rawth": "^3.0.0" + } +} diff --git a/node_modules/@riotjs/util/LICENSE b/node_modules/@riotjs/util/LICENSE new file mode 100644 index 0000000..74e6791 --- /dev/null +++ b/node_modules/@riotjs/util/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Gianluca Guarini + +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. diff --git a/node_modules/@riotjs/util/README.md b/node_modules/@riotjs/util/README.md new file mode 100644 index 0000000..5e1cccb --- /dev/null +++ b/node_modules/@riotjs/util/README.md @@ -0,0 +1,19 @@ +# @riotjs/util + +Riot.js shared util scripts + +[![Build Status][ci-image]][ci-url] +[![Issue Count][qlty-image]][qlty-url] +[![NPM version][npm-version-image]][npm-url] +[![NPM downloads][npm-downloads-image]][npm-url] +[![MIT License][license-image]][license-url] + +[ci-image]: https://img.shields.io/github/actions/workflow/status/riot/util/test.yml?style=flat-square +[ci-url]: https://github.com/riot/util/actions +[license-image]: https://img.shields.io/badge/license-MIT-000000.svg?style=flat-square +[license-url]: LICENSE +[npm-version-image]: https://img.shields.io/npm/v/@riotjs/util.svg?style=flat-square +[npm-downloads-image]: https://img.shields.io/npm/dm/@riotjs/util.svg?style=flat-square +[npm-url]: https://npmjs.org/package/@riotjs/util +[qlty-image]: https://qlty.sh/gh/riot/projects/util/maintainability.svg +[qlty-url]: https://qlty.sh/gh/riot/projects/util diff --git a/node_modules/@riotjs/util/binding-types.cjs b/node_modules/@riotjs/util/binding-types.cjs new file mode 100644 index 0000000..1dc5603 --- /dev/null +++ b/node_modules/@riotjs/util/binding-types.cjs @@ -0,0 +1,24 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +const EACH = 0; +const IF = 1; +const SIMPLE = 2; +const TAG = 3; +const SLOT = 4; + +var bindingTypes = { + EACH, + IF, + SIMPLE, + TAG, + SLOT, +}; + +exports.EACH = EACH; +exports.IF = IF; +exports.SIMPLE = SIMPLE; +exports.SLOT = SLOT; +exports.TAG = TAG; +exports.default = bindingTypes; diff --git a/node_modules/@riotjs/util/binding-types.js b/node_modules/@riotjs/util/binding-types.js new file mode 100644 index 0000000..cc82714 --- /dev/null +++ b/node_modules/@riotjs/util/binding-types.js @@ -0,0 +1,13 @@ +export const EACH = 0 +export const IF = 1 +export const SIMPLE = 2 +export const TAG = 3 +export const SLOT = 4 + +export default { + EACH, + IF, + SIMPLE, + TAG, + SLOT, +} diff --git a/node_modules/@riotjs/util/checks.cjs b/node_modules/@riotjs/util/checks.cjs new file mode 100644 index 0000000..1dc5076 --- /dev/null +++ b/node_modules/@riotjs/util/checks.cjs @@ -0,0 +1,96 @@ +'use strict'; + +var constants = require('./constants.cjs'); + +/** + * Quick type checking + * @param {*} element - anything + * @param {string} type - type definition + * @returns {boolean} true if the type corresponds + */ +function checkType(element, type) { + return typeof element === type +} + +/** + * Check if an element is part of an svg + * @param {HTMLElement} el - element to check + * @returns {boolean} true if we are in an svg context + */ +function isSvg(el) { + const owner = el.ownerSVGElement; + + return !!owner || owner === null +} + +/** + * Check if an element is a template tag + * @param {HTMLElement} el - element to check + * @returns {boolean} true if it's a