@ -0,0 +1,34 @@ | |||||
<!DOCTYPE html> | |||||
<html> | |||||
<head lang="en"> | |||||
<meta charset="utf-8"> | |||||
<title>Tiny Hamburger | Demo 386 Top</title> | |||||
<link rel="shortcut icon" href="#" /> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" type="text/css" rel="stylesheet" /> | |||||
<link href="css/icomoon.css" type="text/css" rel="stylesheet" /> | |||||
<link href="css/fonts.css" type="text/css" rel="stylesheet" /> | |||||
<link href="css/demo-386-top.css" type="text/css" rel="stylesheet" /> | |||||
</head> | |||||
<body> | |||||
<div class="panel"> | |||||
<tiny-hamburger> | |||||
<nav> | |||||
<ul> | |||||
<li><a class="current" href="index.html">Demo: 386 / Top</a></li> | |||||
<li><a href="demo-386-left.html">Demo: 386 / Left</a></li> | |||||
<li><a href="demo-flat-left.html">Demo: Flat / Left</a></li> | |||||
</ul> | |||||
</nav> | |||||
</tiny-hamburger> | |||||
</div> | |||||
<footer> | |||||
<a target="_blank" href="https://github.com/HerrHase/tiny-hamburger"> | |||||
<i class="icon icon-github"></i> | |||||
</a> | |||||
</footer> | |||||
<script src="js/demo-386-top.js"></script> | |||||
</body> | |||||
</html> |
@ -0,0 +1 @@ | |||||
../acorn/bin/acorn |
@ -0,0 +1 @@ | |||||
../cssesc/bin/cssesc |
@ -0,0 +1 @@ | |||||
../esprima/bin/esparse.js |
@ -0,0 +1 @@ | |||||
../esprima/bin/esvalidate.js |
@ -0,0 +1,378 @@ | |||||
# Compiler Changes | |||||
### v4.3.11 | |||||
- Fix https://github.com/riot/compiler/issues/127 | |||||
- Fix https://github.com/riot/compiler/issues/126 | |||||
- Improve the code maintainability refactoring big files into smaller ones | |||||
### v4.3.10 | |||||
- Fix https://github.com/riot/riot/issues/2753 | |||||
- Fix https://github.com/riot/riot/issues/2748 | |||||
- Update acorn to v7.0.0 | |||||
### v4.3.9 | |||||
- Fix https://github.com/riot/compiler/issues/125 | |||||
- Fix https://github.com/riot/compiler/issues/124 | |||||
### v4.3.8 | |||||
- Fix make sure that the `createExpression` internal function receives always all its arguments | |||||
### v4.3.7 | |||||
- Fix https://github.com/riot/compiler/issues/121 | |||||
### v4.3.6 | |||||
- Fix https://github.com/riot/compiler/issues/122 | |||||
- Fix https://github.com/riot/compiler/pull/118 | |||||
### v4.3.5 | |||||
- Fix backslashed unicode css properties | |||||
### v4.3.4 | |||||
- Fix escape backslashes in css strings https://github.com/riot/riot/issues/2726 | |||||
### v4.3.3 | |||||
- Fix https://github.com/riot/compiler/issues/119 | |||||
- Fix https://github.com/riot/riot/issues/2726 | |||||
### v4.3.2 | |||||
- Fix void tags will be automatically corrected for example: | |||||
```html | |||||
<svg> | |||||
<circle></circle> | |||||
</svg> | |||||
``` | |||||
Will be transfromed to | |||||
```html | |||||
<svg> | |||||
<circle/> | |||||
</svg> | |||||
``` | |||||
### v4.3.1 | |||||
- Fix https://github.com/riot/riot/issues/2719 | |||||
- Fix https://github.com/riot/riot/issues/2723 | |||||
### v4.3.0 | |||||
- Add support for dynamic import | |||||
### v4.2.6 | |||||
- Fix expression parts issues https://github.com/riot/riot/issues/2701 | |||||
### v4.2.5 | |||||
- Fix https://github.com/riot/riot/issues/2700 replacing the `esprima` parser with `acorn` | |||||
### v4.2.4 | |||||
- Fix attributes on custom tags having `if` or `each` directives | |||||
### v4.2.3 | |||||
- Update `@riotjs/dom-bindings` using v4.0.0 | |||||
- Update npm dependencies | |||||
### v4.2.2 | |||||
- Fix [riot#2691](https://github.com/riot/riot/issues/2691) | |||||
### v4.2.1 | |||||
- Fix css generation with `@media` queries | |||||
### v4.2.0 | |||||
- Add support for `<a {href}/>` shortcut expressions | |||||
- Fix spread expressions issue [riot/2679](https://github.com/riot/riot/issues/2679) | |||||
### v4.1.1 | |||||
- Fix commonjs imports | |||||
### v4.1.0 | |||||
- Add support for the slot attribute binding | |||||
### v4.0.4 | |||||
- Fix avoid removing selector attributes twice on custom tags | |||||
### v4.0.3 | |||||
- Fix attributes handling on custom children nodes [riot#2680](https://github.com/riot/riot/issues/2680) | |||||
### v4.0.2 | |||||
- Fix spread operator on each directives [riot#2679](https://github.com/riot/riot/issues/2679) | |||||
### v4.0.1 | |||||
- Fix attributes mixed with expressions [riot#2681](https://github.com/riot/riot/issues/2681) | |||||
### v4.0.0 | |||||
- Complete rewrite **not backward compatible** | |||||
- New output compatible only for Riot.js 4.0.0 | |||||
- Add better sourcemaps generation | |||||
- Add the `@riotjs/parser` fixing odd issues with regex like [#114](https://github.com/riot/compiler/issues/114) | |||||
- Improve the code generation strategy preferring AST to regex parsing | |||||
- Remove all the preprocessors from the core in favor of `registerPreprocessor` and `registerPostprocessor` instead | |||||
### v4.0.0-beta.5 | |||||
- Fix https://github.com/riot/riot/issues/2669 | |||||
### v4.0.0-beta.3 | |||||
- Fix https://github.com/riot/compiler/issues/115 | |||||
### v4.0.0-beta.2 | |||||
- Add support for multiple expressions on the same attribute node | |||||
### v4.0.0-beta.1 | |||||
- Update rename the `tag` property `exports` | |||||
### v4.0.0-alpha.20 | |||||
- Fix handle escaped chars | |||||
### v4.0.0-alpha.19 | |||||
- Fix bug in nodes with a single expression | |||||
### v4.0.0-alpha.18 | |||||
- Add the `name` key to the tag exports | |||||
- Fix self-closed tag bindings | |||||
### v4.0.0-alpha.17 | |||||
- Remove unused dev dependencies | |||||
### v4.0.0-alpha.16 | |||||
- *Breaking change*: make the compiler API synchronous | |||||
### v4.0.0-alpha.15 | |||||
- Fix slots root nodes handling | |||||
### v4.0.0-alpha.14 | |||||
- Add sourcemap tests for babel preprocessor | |||||
- Update handling of multiple line text expressions, from template literal to array | |||||
- Update output format | |||||
### v4.0.0-alpha.13 | |||||
- Fix sourcemap for the multiple text expressions | |||||
- Check make sure that `slot` tags will not be considered custom tags | |||||
### v4.0.0-alpha.12 | |||||
- Fix sourcemap `sourcesContent` property | |||||
- Update sourcemap filename | |||||
### v4.0.0-alpha.11 | |||||
- Fix sourcemap generation for the `if` and `each` tag bindings | |||||
### v4.0.0-alpha.10 | |||||
- Update enhance sourcemaps generation | |||||
- Change second arguments for the pre/post processors. The `meta` object will contain info about the current compilation | |||||
### v4.0.0-alpha.9 | |||||
- Fix move `recast` into the package dependencies | |||||
### v4.0.0-alpha.8 | |||||
- Enhance the source map generation | |||||
- Improve performance | |||||
- Update npm dependencies | |||||
### v4.0.0-alpha.7 | |||||
- Add support for the scoped css | |||||
- Update the Tag bindings output to support dynamic tags | |||||
### v4.0.0-alpha.6 | |||||
- Fix issue with the object expressions scoping | |||||
### v4.0.0-alpha.5 | |||||
- Update the tag bindings API to get the component implementation via function | |||||
### v4.0.0-alpha.4 | |||||
- Fix issues related to the member expressions traversal and the scope | |||||
### v4.0.0-alpha.3 | |||||
- Fix issue with custom tags and no slots | |||||
### v4.0.0-alpha.2 | |||||
- Add support for the spread attributes `<a {...foo}/>` | |||||
- Add the `tagName` to the compiler options in runtime | |||||
- Fix the options were not passed to the postprocessor | |||||
### v4.0.0-alpha.1 | |||||
- New complete rewrite from scratch | |||||
- Change npm name from `riot-compiler` to `@riotjs/compiler` | |||||
- First alpha release not backward compatible | |||||
### v3.5.2 | |||||
- Fix es6 dynamic imports https://github.com/riot/riot/issues/2641 | |||||
### v3.5.1 | |||||
- Fix try importing `@babel/core` first and then fallback to `babel-core` for the `es6` parser | |||||
### v3.5.0 | |||||
- Add support for Babel 7 | |||||
### v3.4.0 | |||||
- Add inline sourcemap support via `sourcemap='inline'` option | |||||
### v3.3.1 | |||||
- Improve the sourcemap generation adding the `sourceContent` key | |||||
### v3.3.0 | |||||
- Add initial experimental sourcemaps support via `sourcemap: true` option | |||||
### v3.2.6 | |||||
- Fix #105 | |||||
- Fix #104 | |||||
### v3.2.5 | |||||
- Update dependencies and refactor some internal code avoiding bitwise operators | |||||
- Fix coffeescript parser require https://github.com/riot/compiler/pull/102 | |||||
### v3.2.4 | |||||
- Fix [riot#2369](https://github.com/riot/riot/issues/2369) : Possible bug involving compilation of tags containing regex. | |||||
- Using the `skip-regex` function from npm for sharing bwteen modules (at future). | |||||
- Using the `jsSplitter` function for safer replacement of JS code, part of the next compiler. | |||||
### v3.2.3 | |||||
- Fixes various issues with literal regexes. | |||||
### v3.1.4 | |||||
- Fix avoid the `filename` option for the babel-standalone parser | |||||
### v3.1.3 | |||||
- Fix babel in browser runtime parser https://github.com/riot/examples/issues/51 | |||||
### v3.1.2 | |||||
- Fix [riot#2210](https://github.com/riot/riot/issues/2210) : Style tag get stripped from riot tag even if it's in a javascript string. | |||||
- Updated devDependencies. | |||||
### v3.1.0 | |||||
- Adds support for css @apply rule: now ScopedCSS parser can handle it properly | |||||
### v3.0.0 | |||||
- Deprecate old `babel` support, now the `es6` parser will use Babel 6 by default | |||||
- Change css always scoped by default | |||||
- Fix all the `value` attributes using expressions will be output as `riot-value` to [riot#1957](https://github.com/riot/riot/issues/1957) | |||||
### v2.5.5 | |||||
- Fix to erroneous version number in the package.json, v2.5.4 was released before. | |||||
- Removed unuseful files from the npm package. | |||||
- Updated credits in package.json | |||||
- Updated devDependencies, skip ESLint in CI test for node v0.12 | |||||
- BuGless-hack for [riot#1966](https://github.com/riot/riot/issues/1966) - You can use `<-/>` to signal the end of the html if your html is ending with an expression. | |||||
### v2.5.4 | |||||
- Fix #68 : SASS inside Pug template gives Invalid CSS. | |||||
- Added parser for [bublé](https://buble.surge.sh) as `buble` in the browser. Option `modules` is `false` in all versions. | |||||
- Added parser for [bublé](https://buble.surge.sh) as `buble`. | |||||
- Added support for es6 `import` statements. Thanks to @kuashe! - Related to [riot#1715](https://github.com/riot/riot/issues/1715), [riot#1784](https://github.com/riot/riot/issues/1784), and [riot#1864](https://github.com/riot/riot/issues/1864). | |||||
### v2.5.3 | |||||
- Fix #73 : resolveModuleSource must be a function - Option removed from the default Babel options. | |||||
- Updated node.js to 4.4 in the Travis environment. | |||||
- Downgraded ESLint to 2.x for using with node v0.12.x | |||||
### v2.5.2 | |||||
- Fix #72: `undefined` is not a function when evaluating `parsers._req`. | |||||
- Updated node versions for travis, including v5.x | |||||
### v2.4.1 | |||||
- Add the `pug` parser (it will replace completely `jade` in the next major release) | |||||
- Add the possibility to pass custom parsers options directly via the `compiler.compile` method through the `parserOptions: {js: {}, template: {}, style: {}}` key [more info](https://github.com/riot/compiler/issues/64) | |||||
- Fix un-escape parser options in html [more info](https://github.com/riot/compiler/issues/63) | |||||
### v2.3.23 | |||||
- The parsers are moved to its own directory in the node version. The load is on first use. | |||||
- Fix [riot#1325](https://github.com/riot/riot/issues/1325) : Gulp + Browserify + Babelify + type="es6" error. | |||||
- Fix [riot#1342](https://github.com/riot/riot/issues/1342), [riot#1636](https://github.com/riot/riot/issues/1636) and request from [dwyl/learn-riot#8](https://github.com/dwyl/learn-riot/issues/8) : Server-Side Rendered Page Fails W3C Check. The new `data-is` attribute is used for scoped styles in addition to `riot-tag` (the later will be removed in compiler v3.x) | |||||
- The keyword `defer` in `<script src=file>` avoids that the compiler loads the file, preserving the tag - Requested by [riot#1492](https://github.com/riot/riot/issues/1492) : Stop script tags from being evaluated with serverside `riot.render`. It is removed in client-side compilation because browsers will not load scripts through innerHTML. | |||||
- It has changed the character used to hide expressions during the compilation, maybe this fix [riot#1588](https://github.com/riot/riot/issues/1588) : Syntax Error: Invalid character `\0129` (riot+compiler.min). | |||||
- The option `debug` inserts newlines between the `riot.tag2` parameters and the call is prefixed with the source filename - Requested by [riot#1646](https://github.com/riot/riot/issues/1646) : Split portions of generated html with newline instead of space | |||||
- Removed the unused parameter with the compiled-time brackets from the call to `riot.tag2`. | |||||
- Removed support for raw expressions. It is unlikely this feature will be implemented in v2.3.x | |||||
- Updated the regex that is used to match tag names, more closer to the HTML5 specs. | |||||
- Update devDependencies. | |||||
### v2.3.22 | |||||
- Fix [riot#1511](https://github.com/riot/riot/issues/1511) : Escape Quotes - They may be some issues to fix. | |||||
- Regression of logic to parse style and script tags, due to loss of performance and other issues. | |||||
- Removed the "compress" option of the `less` parser, which is deprecated and generates warnings in the console. | |||||
- Removed the unuseful CSS parser `stylus` from the browser version. | |||||
- Refactorization of all the code, with more comments in preparation for the automatic documentation of the API. | |||||
- Various tweaks to increase performance and reduce (~55%) memory consumption. | |||||
- Files to preprocess are moved from "lib" to the "src" directory, now "lib" has the required node.js files only. | |||||
### v2.3.21 (unpublished due to errors) | |||||
### v2.3.20 | |||||
- Fix [riot#1495](https://github.com/riot/riot/issues/1495) : Warning of input tag value - Avoids warnings for date/datetime/time/month/email/color types with expression in its value. | |||||
- Fix [riot#1488](https://github.com/riot/riot/issues/1488) : Cannot read property 'replace' of undefined when compiling in Node a tag with an import in its less stylesheet -- Thanks to @jrx-jsj | |||||
- Fix [riot#1448](https://github.com/riot/riot/issues/1448) : Riot compiler parses and removes content from string declaration. This is partial fix, you need to write `<\/script>` for closing script tags within quoted strings. | |||||
- Revised regex that matches `<pre>` tags. | |||||
- `@import` directives of `stylus`, `sass`, `scss`, and `less` can be relative to the file being processed. | |||||
- Fixed lint issues with new .eslintrc.yml, almost compatible with [JavaScript Standard Style](http://standardjs.com/) | |||||
### v2.3.19 | |||||
- Fixing issues with double quotes. | |||||
- Removed dependency on riot-tmpl for the node build, now we are using a local version of `brackets`. | |||||
### v2.3.18 | |||||
- Regression of optimized regexes not working in IE9/10. | |||||
- Fix #36 : removed the excluded strings from the ouput. | |||||
- Fix: avoid changing the global brackets when the compiler is called with other brackets (requires riot-tmpl v2.3.15). | |||||
- A new property `version` (string) is included in the compiler set. | |||||
- Fixes to travis CI and the bump routine | |||||
### v2.3.15 (unpublished from npm) | |||||
- Preparation for use as ES6 module through [rollup.js](http://rollupjs.org/) | |||||
- Update devDependencies, including jspreproc v0.2.5 with an important fix. | |||||
- Partial regression of fix [riot#1120](https://github.com/riot/riot/issues/1120), `tmpl` can parse double-quotes within expressions, encoding double-quotes generates issues. | |||||
### v2.3.14 | |||||
- The prefix `__` for boolean attributes is not used anymore. This IE8 hack and it is not neccessary for current supported versions. | |||||
- Option `exclude` for ignore parts of the tag. This is an array with one or more of 'html', 'css', 'attribs', 'js'. | |||||
- Removed `inert` from the boolean attributes list, this html5 attribute was dropped from the specs. | |||||
- Fixed normalization of root attributes, was not working as expected. Example updated. | |||||
### v2.3.13 | |||||
- Fixed the `style` option for setting the CSS parser through the `options` object. | |||||
- Fixed an issue in preservation of line endings in the generated html markup. | |||||
- Fixed tests, coverage is 100% again. | |||||
- Updated [doc/guide.md](https://github.com/riot/compiler/blob/master/doc/guide.md) and [doc/attributes.md](https://github.com/riot/compiler/blob/master/doc/attrbutes.md) with the latest features. | |||||
### v2.3.12 | |||||
- Gets rid of the zero-indentation restriction for custom tags, now you can indent these tags, but the opening and closing tag must have exactly the same indentation (length and type). All the tag will be unindented by this amount. | |||||
- Support for `src` and `charset` attributes in `<script>` tags for reading JavaScript sources from the file system - [riot#1116](https://github.com/riot/riot/issues/1116), [riot#507](https://github.com/riot/riot/issues/507) | |||||
- The `compile` function can return separate parts by setting the new `entities` option. These parts has unescaped newlines. | |||||
- New attribute `options` for `script` and `style` tags will append/overwrite attributes in the default configuration object of the parser at tag level. | |||||
- Fix [riot#1261](https://github.com/riot/riot/issues/1261) : `<pre>` tag does not preserve neither `\n` nor `\t`. | |||||
Now whitespace within `<pre>` tags is always preserved. | |||||
- Fix [riot#1358](https://github.com/riot/riot/issues/1358) : Empty style in tag (scoped) breaks. | |||||
### v2.3.11 | |||||
- New type="babel" supports babel-core v6.x. You must `npm install babel-preset-es2015` too, for this works. | |||||
Use type="es6" for babel and babel-core v5.8.x and bellow - [riot#1039](https://github.com/riot/riot/issues/1039) | |||||
- Fix [riot#1306](https://github.com/riot/riot/issues/1306) : Compiler preserves newlines in class objects, causing "Unterminated String Constant" errors. | |||||
- Fix [riot#1314](https://github.com/riot/riot/issues/1314) : `settings.brackets` no longer works. | |||||
- Fix [riot#1309](https://github.com/riot/riot/issues/1309) : Tag renders js instead of content when no attributes present. | |||||
### v2.3.0 | |||||
This is a complete rewrite and the first solo version of the compiler. | |||||
- Now the compiler generates a call to the new `riot.tag2` function, with the same parameters as `riot.tag` and an | |||||
additional one: the brackets used in the compilation. `riot.tag2` requires all parameters except the brackets, | |||||
so the compiler generates all but ignores the brackets if there are no generated expressions. | |||||
- Unlike previous versions, backslashes are removed from the expressions (before being sent to any parser). | |||||
Outside of expressions, all backslashes are preserved. | |||||
- Double quotes inside expressions are converted to `"`, to avoid issues with HTML markup. | |||||
- Fix [riot#1207](https://github.com/riot/riot/issues/1207) : Riot compiler/parser breaks indentation. | |||||
- Fix [riot#1120](https://github.com/riot/riot/issues/1120) : Double quotes break Riot attributes | |||||
Enhancements | |||||
- The compiler loads the brackets in runtime on each tag, allowing use of different brackets. [riot#1122](https://github.com/riot/riot/issues/1122) related. | |||||
- Multiple `<script>` blocks. These can have different types and are merged with the untagged script block, if any. | |||||
- More flexible formats in ES6 style method definitions. | |||||
- In the JavaScript, trailing whitespace are removed and multiple empty lines are combined into one. | |||||
- Better recognition of expressions. Now you can use almost any character, even in unquoted expressions (expressions containing the `>` operator needs to be enclosed in quotes) - [riot#744](https://github.com/riot/riot/issues/744) | |||||
- If the first character inside an expression is `^`, the expression is not passed to any parser. This is some sort of type=none at expression level - [riot#543](https://github.com/riot/riot/issues/543), [riot#1090](https://github.com/riot/riot/issues/1090) | |||||
- Type es6 now supports babel-core - [riot#1039](https://github.com/riot/riot/issues/1039) | |||||
- New logic for scoped style blocks, if a style contains the ":scoped" selector, this is replaced by the name of the root element, if not, the name is prepended - [riot#912](https://github.com/riot/riot/issues/912) | |||||
- `type="scoped-css"` for `style` tags is deprecated, use only `scoped` or `scoped="scoped"` |
@ -0,0 +1,22 @@ | |||||
The MIT License (MIT) | |||||
Copyright (c) 2015 Riot | |||||
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. | |||||
@ -0,0 +1,107 @@ | |||||
[![Build Status][travis-image]][travis-url] | |||||
[![Issue Count][codeclimate-image]][codeclimate-url] | |||||
[![Coverage Status][coverage-image]][coverage-url] | |||||
[![NPM version][npm-version-image]][npm-url] | |||||
[![NPM downloads][npm-downloads-image]][npm-url] | |||||
[![MIT License][license-image]][license-url] | |||||
## Important | |||||
This compiler will not work with older Riot.js versions. | |||||
It's designed to work with Riot.js > 4.0.0. | |||||
For Riot.js < 4.0.0 please check the [v3](https://github.com/riot/compiler/tree/v3) branch | |||||
## Installation | |||||
``` | |||||
npm i @riotjs/compiler -D | |||||
``` | |||||
## Usage | |||||
The riot compiler can compile only strings: | |||||
```js | |||||
import { compile } from '@riotjs/compiler' | |||||
const { code, map } = compile('<p>{hello}</p>') | |||||
``` | |||||
You can compile your tags also using the new `registerPreprocessor` and `registerPostprocessor` APIs for example: | |||||
```js | |||||
import { compiler, registerPreprocessor, registerPostprocessor } from '@riotjs/compiler' | |||||
import pug from 'pug' | |||||
import buble from 'buble' | |||||
// process your tag template before it will be compiled | |||||
registerPreprocessor('template', 'pug', function(code, { options }) { | |||||
const { file } = options | |||||
console.log('your file path is:', file) | |||||
return { | |||||
code: pug.render(code), | |||||
// no sourcemap here | |||||
map: null | |||||
} | |||||
}) | |||||
// your compiler output will pass from here | |||||
registerPostprocessor(function(code, { options }) { | |||||
const { file } = options | |||||
console.log('your file path is:', file) | |||||
// notice that buble.transform returns {code, map} | |||||
return buble.transform(code) | |||||
}) | |||||
const { code, map } = compile('<p>{hello}</p>', { | |||||
// specify the template preprocessor | |||||
template: 'pug' | |||||
}) | |||||
``` | |||||
## API | |||||
### compile(string, options) | |||||
#### @returns `<Promise>{ code, map }` output that can be used by Riot.js | |||||
- *string*: is your tag source code | |||||
- *options*: the options should contain the `file` key identifying the source of the string to compile and | |||||
the `template` preprocessor to use as string | |||||
Note: specific preprocessors like the `css` or the `javascript` ones can be enabled simply specifying the `type` attribute | |||||
in the tag source code for example | |||||
```html | |||||
<my-tag> | |||||
<style type='scss'> | |||||
// ... | |||||
</style> | |||||
</my-tag> | |||||
``` | |||||
### registerPreprocessor(type, id, preprocessorFn) | |||||
#### @returns `Object` containing all the preprocessors registered | |||||
- *type*: either one of `template` `css` or `javascript` | |||||
- *id*: unique preprocessor identifier | |||||
- *preprocessorFn*: function receiving the code as first argument and the current options as second | |||||
### registerPostprocessor(postprocessorFn) | |||||
#### @returns `Set` containing all the postprocessors registered | |||||
- *postprocessorFn*: function receiving the compiler output as first argument and the current options as second | |||||
[travis-image]: https://img.shields.io/travis/riot/compiler.svg?style=flat-square | |||||
[travis-url]: https://travis-ci.org/riot/compiler | |||||
[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/compiler.svg?style=flat-square | |||||
[npm-downloads-image]: https://img.shields.io/npm/dm/@riotjs/compiler.svg?style=flat-square | |||||
[npm-url]: https://npmjs.org/package/@riotjs/compiler | |||||
[coverage-image]: https://img.shields.io/coveralls/riot/compiler/master.svg?style=flat-square | |||||
[coverage-url]: https://coveralls.io/r/riot/compiler?branch=master | |||||
[codeclimate-image]: https://api.codeclimate.com/v1/badges/37de24282e8d113bb0cc/maintainability | |||||
[codeclimate-url]: https://codeclimate.com/github/riot/compiler |
@ -0,0 +1,99 @@ | |||||
{ | |||||
"_from": "@riotjs/compiler@^4.3.11", | |||||
"_id": "@riotjs/compiler@4.3.11", | |||||
"_inBundle": false, | |||||
"_integrity": "sha512-3TpOuoiXWSLGvcvZRfhJLdpRpwJihmT+J+NB2nWXA5/8+23x1soUoPmBtRi9Jo2xLInsh3J1/q5tQ8LjXLc2eQ==", | |||||
"_location": "/@riotjs/compiler", | |||||
"_phantomChildren": {}, | |||||
"_requested": { | |||||
"type": "range", | |||||
"registry": true, | |||||
"raw": "@riotjs/compiler@^4.3.11", | |||||
"name": "@riotjs/compiler", | |||||
"escapedName": "@riotjs%2fcompiler", | |||||
"scope": "@riotjs", | |||||
"rawSpec": "^4.3.11", | |||||
"saveSpec": null, | |||||
"fetchSpec": "^4.3.11" | |||||
}, | |||||
"_requiredBy": [ | |||||
"/riot" | |||||
], | |||||
"_resolved": "https://registry.npmjs.org/@riotjs/compiler/-/compiler-4.3.11.tgz", | |||||
"_shasum": "b068216c19092d524dc0a7fbce1572a23830607b", | |||||
"_spec": "@riotjs/compiler@^4.3.11", | |||||
"_where": "/home/herrhase/Workspace/tentakelfabrik/tiny-components/tiny-one-page/node_modules/riot", | |||||
"author": { | |||||
"name": "Gianluca Guarini", | |||||
"email": "gianluca.guarini@gmail.com", | |||||
"url": "http://gianlucaguarini.com" | |||||
}, | |||||
"bugs": { | |||||
"url": "https://github.com/riot/compiler/issues" | |||||
}, | |||||
"bundleDependencies": false, | |||||
"dependencies": { | |||||
"@riotjs/dom-bindings": "^4.2.5", | |||||
"@riotjs/parser": "^4.0.3", | |||||
"acorn": "^7.0.0", | |||||
"cssesc": "^3.0.0", | |||||
"cumpa": "^1.0.1", | |||||
"curri": "^1.0.1", | |||||
"dom-nodes": "^1.1.3", | |||||
"globals": "^12.0.0", | |||||
"recast": "^0.18.2", | |||||
"source-map": "^0.7.3" | |||||
}, | |||||
"deprecated": false, | |||||
"description": "Compiler for riot .tag files", | |||||
"devDependencies": { | |||||
"chai": "^4.2.0", | |||||
"coveralls": "^3.0.6", | |||||
"eslint": "^6.2.1", | |||||
"eslint-config-riot": "^3.0.0", | |||||
"esm": "^3.2.25", | |||||
"mocha": "^6.2.0", | |||||
"nyc": "^14.1.1", | |||||
"rollup": "^1.20.1", | |||||
"rollup-plugin-alias": "^2.0.0", | |||||
"rollup-plugin-commonjs": "^10.0.2", | |||||
"rollup-plugin-json": "^4.0.0", | |||||
"rollup-plugin-node-builtins": "^2.1.2", | |||||
"rollup-plugin-node-resolve": "^5.2.0", | |||||
"shelljs": "^0.8.3" | |||||
}, | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"homepage": "https://github.com/riot/compiler#readme", | |||||
"jsnext:main": "dist/index.esm.js", | |||||
"keywords": [ | |||||
"riot", | |||||
"Riot.js", | |||||
"components", | |||||
"custom components", | |||||
"custom elements", | |||||
"compiler" | |||||
], | |||||
"license": "MIT", | |||||
"main": "dist/index.js", | |||||
"module": "dist/index.esm.js", | |||||
"name": "@riotjs/compiler", | |||||
"repository": { | |||||
"type": "git", | |||||
"url": "git+https://github.com/riot/compiler.git" | |||||
}, | |||||
"scripts": { | |||||
"build": "rollup -c build/rollup.node.config.js && rollup -c build/rollup.browser.config.js", | |||||
"cov": "nyc report --reporter=text-lcov | coveralls", | |||||
"cov-html": "nyc report --reporter=html", | |||||
"debug": "mocha --inspect --inspect-brk -r esm test/*.spec.js test/**/*.spec.js", | |||||
"lint": "eslint src/ test/ build/", | |||||
"postest": "npm run cov-html", | |||||
"prepare": "npm i pug@2.0.3 node-sass@4.12.0 @babel/core@7 @babel/preset-env@7 --no-save", | |||||
"prepublishOnly": "npm run build && npm run test", | |||||
"test": "npm run lint && nyc mocha -r esm test/*.spec.js test/**/*.spec.js" | |||||
}, | |||||
"version": "4.3.11" | |||||
} |
@ -0,0 +1,4 @@ | |||||
export const TAG_LOGIC_PROPERTY = 'exports' | |||||
export const TAG_CSS_PROPERTY = 'css' | |||||
export const TAG_TEMPLATE_PROPERTY = 'template' | |||||
export const TAG_NAME_PROPERTY = 'name' |
@ -0,0 +1,124 @@ | |||||
import {builders, types} from '../../utils/build-types' | |||||
import {TAG_CSS_PROPERTY} from '../../constants' | |||||
import cssEscape from 'cssesc' | |||||
import getPreprocessorTypeByAttribute from '../../utils/get-preprocessor-type-by-attribute' | |||||
import preprocess from '../../utils/preprocess-node' | |||||
/** | |||||
* Matches valid, multiline JavaScript comments in almost all its forms. | |||||
* @const {RegExp} | |||||
* @static | |||||
*/ | |||||
const R_MLCOMMS = /\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//g | |||||
/** | |||||
* Source for creating regexes matching valid quoted, single-line JavaScript strings. | |||||
* It recognizes escape characters, including nested quotes and line continuation. | |||||
* @const {string} | |||||
*/ | |||||
const S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source | |||||
/** | |||||
* Matches CSS selectors, excluding those beginning with '@' and quoted strings. | |||||
* @const {RegExp} | |||||
*/ | |||||
const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|${S_LINESTR}`, 'g') | |||||
/** | |||||
* Parses styles enclosed in a "scoped" tag | |||||
* The "css" string is received without comments or surrounding spaces. | |||||
* | |||||
* @param {string} tag - Tag name of the root element | |||||
* @param {string} css - The CSS code | |||||
* @returns {string} CSS with the styles scoped to the root element | |||||
*/ | |||||
function scopedCSS(tag, css) { | |||||
const host = ':host' | |||||
const selectorsBlacklist = ['from', 'to'] | |||||
return css.replace(CSS_SELECTOR, function(m, p1, p2) { | |||||
// skip quoted strings | |||||
if (!p2) return m | |||||
// we have a selector list, parse each individually | |||||
p2 = p2.replace(/[^,]+/g, function(sel) { | |||||
const s = sel.trim() | |||||
// skip selectors already using the tag name | |||||
if (s.indexOf(tag) === 0) { | |||||
return sel | |||||
} | |||||
// skips the keywords and percents of css animations | |||||
if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') { | |||||
return sel | |||||
} | |||||
// replace the `:host` pseudo-selector, where it is, with the root tag name; | |||||
// if `:host` was not included, add the tag name as prefix, and mirror all | |||||
// `[data-is]` | |||||
if (s.indexOf(host) < 0) { | |||||
return `${tag} ${s},[is="${tag}"] ${s}` | |||||
} else { | |||||
return `${s.replace(host, tag)},${ | |||||
s.replace(host, `[is="${tag}"]`)}` | |||||
} | |||||
}) | |||||
// add the danling bracket char and return the processed selector list | |||||
return p1 ? `${p1} ${p2}` : p2 | |||||
}) | |||||
} | |||||
/** | |||||
* Remove comments, compact and trim whitespace | |||||
* @param { string } code - compiled css code | |||||
* @returns { string } css code normalized | |||||
*/ | |||||
function compactCss(code) { | |||||
return code.replace(R_MLCOMMS, '').replace(/\s+/g, ' ').trim() | |||||
} | |||||
const escapeBackslashes = s => s.replace(/\\/g, '\\\\') | |||||
const escapeIdentifier = identifier => escapeBackslashes(cssEscape(identifier, { | |||||
isIdentifier: true | |||||
})) | |||||
/** | |||||
* Generate the component css | |||||
* @param { Object } sourceNode - node generated by the riot compiler | |||||
* @param { string } source - original component source code | |||||
* @param { Object } meta - compilation meta information | |||||
* @param { AST } ast - current AST output | |||||
* @returns { AST } the AST generated | |||||
*/ | |||||
export default function css(sourceNode, source, meta, ast) { | |||||
const preprocessorName = getPreprocessorTypeByAttribute(sourceNode) | |||||
const { options } = meta | |||||
const preprocessorOutput = preprocess('css', preprocessorName, meta, sourceNode.text) | |||||
const normalizedCssCode = compactCss(preprocessorOutput.code) | |||||
const escapedCssIdentifier = escapeIdentifier(meta.tagName) | |||||
const cssCode = (options.scopedCss ? | |||||
scopedCSS(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) : | |||||
escapeBackslashes(normalizedCssCode) | |||||
).trim() | |||||
types.visit(ast, { | |||||
visitProperty(path) { | |||||
if (path.value.key.value === TAG_CSS_PROPERTY) { | |||||
path.value.value = builders.templateLiteral( | |||||
[builders.templateElement({ raw: cssCode, cooked: '' }, false)], | |||||
[] | |||||
) | |||||
return false | |||||
} | |||||
this.traverse(path) | |||||
} | |||||
}) | |||||
return ast | |||||
} |
@ -0,0 +1,92 @@ | |||||
import {TAG_LOGIC_PROPERTY} from '../../constants' | |||||
import addLinesOffset from '../../utils/add-lines-offset' | |||||
import generateAST from '../../utils/generate-ast' | |||||
import getPreprocessorTypeByAttribute from '../../utils/get-preprocessor-type-by-attribute' | |||||
import isEmptySourcemap from '../../utils/is-empty-sourcemap' | |||||
import {isExportDefaultStatement} from '../../utils/ast-nodes-checks' | |||||
import preprocess from '../../utils/preprocess-node' | |||||
import sourcemapToJSON from '../../utils/sourcemap-as-json' | |||||
import {types} from '../../utils/build-types' | |||||
/** | |||||
* Find the export default statement | |||||
* @param { Array } body - tree structure containing the program code | |||||
* @returns { Object } node containing only the code of the export default statement | |||||
*/ | |||||
function findExportDefaultStatement(body) { | |||||
return body.find(isExportDefaultStatement) | |||||
} | |||||
/** | |||||
* Find all the code in an ast program except for the export default statements | |||||
* @param { Array } body - tree structure containing the program code | |||||
* @returns { Array } array containing all the program code except the export default expressions | |||||
*/ | |||||
function filterNonExportDefaultStatements(body) { | |||||
return body.filter(node => !isExportDefaultStatement(node)) | |||||
} | |||||
/** | |||||
* Get the body of the AST structure | |||||
* @param { Object } ast - ast object generated by recast | |||||
* @returns { Array } array containing the program code | |||||
*/ | |||||
function getProgramBody(ast) { | |||||
return ast.body || ast.program.body | |||||
} | |||||
/** | |||||
* Extend the AST adding the new tag method containing our tag sourcecode | |||||
* @param { Object } ast - current output ast | |||||
* @param { Object } exportDefaultNode - tag export default node | |||||
* @returns { Object } the output ast having the "tag" key extended with the content of the export default | |||||
*/ | |||||
function extendTagProperty(ast, exportDefaultNode) { | |||||
types.visit(ast, { | |||||
visitProperty(path) { | |||||
if (path.value.key.value === TAG_LOGIC_PROPERTY) { | |||||
path.value.value = exportDefaultNode.declaration | |||||
return false | |||||
} | |||||
this.traverse(path) | |||||
} | |||||
}) | |||||
return ast | |||||
} | |||||
/** | |||||
* Generate the component javascript logic | |||||
* @param { Object } sourceNode - node generated by the riot compiler | |||||
* @param { string } source - original component source code | |||||
* @param { Object } meta - compilation meta information | |||||
* @param { AST } ast - current AST output | |||||
* @returns { AST } the AST generated | |||||
*/ | |||||
export default function javascript(sourceNode, source, meta, ast) { | |||||
const preprocessorName = getPreprocessorTypeByAttribute(sourceNode) | |||||
const javascriptNode = addLinesOffset(sourceNode.text.text, source, sourceNode) | |||||
const { options } = meta | |||||
const preprocessorOutput = preprocess('javascript', preprocessorName, meta, { | |||||
...sourceNode, | |||||
text: javascriptNode | |||||
}) | |||||
const inputSourceMap = sourcemapToJSON(preprocessorOutput.map) | |||||
const generatedAst = generateAST(preprocessorOutput.code, { | |||||
sourceFileName: options.file, | |||||
inputSourceMap: isEmptySourcemap(inputSourceMap) ? null : inputSourceMap | |||||
}) | |||||
const generatedAstBody = getProgramBody(generatedAst) | |||||
const bodyWithoutExportDefault = filterNonExportDefaultStatements(generatedAstBody) | |||||
const exportDefaultNode = findExportDefaultStatement(generatedAstBody) | |||||
const outputBody = getProgramBody(ast) | |||||
// add to the ast the "private" javascript content of our tag script node | |||||
outputBody.unshift(...bodyWithoutExportDefault) | |||||
// convert the export default adding its content to the "tag" property exported | |||||
if (exportDefaultNode) extendTagProperty(ast, exportDefaultNode) | |||||
return ast | |||||
} |
@ -0,0 +1,110 @@ | |||||
import { | |||||
BINDING_CONDITION_KEY, | |||||
BINDING_EVALUATE_KEY, | |||||
BINDING_GET_KEY_KEY, | |||||
BINDING_INDEX_NAME_KEY, | |||||
BINDING_ITEM_NAME_KEY, | |||||
BINDING_TYPES, | |||||
BINDING_TYPE_KEY, | |||||
EACH_BINDING_TYPE | |||||
} from '../constants' | |||||
import { | |||||
createASTFromExpression, | |||||
createSelectorProperties, | |||||
createTemplateProperty, | |||||
getAttributeExpression, | |||||
getName, | |||||
toScopedFunction | |||||
} from '../utils' | |||||
import { findEachAttribute, findIfAttribute, findKeyAttribute } from '../find' | |||||
import {isExpressionStatement, isSequenceExpression} from '../../../utils/ast-nodes-checks' | |||||
import {nullNode, simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
import {builders} from '../../../utils/build-types' | |||||
import compose from 'cumpa' | |||||
import {createNestedBindings} from '../builder' | |||||
import generateJavascript from '../../../utils/generate-javascript' | |||||
import panic from '../../../utils/panic' | |||||
const getEachItemName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[0] : expression.left | |||||
const getEachIndexName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[1] : null | |||||
const getEachValue = expression => expression.right | |||||
const nameToliteral = compose(builders.literal, getName) | |||||
const generateEachItemNameKey = expression => simplePropertyNode( | |||||
BINDING_ITEM_NAME_KEY, | |||||
compose(nameToliteral, getEachItemName)(expression) | |||||
) | |||||
const generateEachIndexNameKey = expression => simplePropertyNode( | |||||
BINDING_INDEX_NAME_KEY, | |||||
compose(nameToliteral, getEachIndexName)(expression) | |||||
) | |||||
const generateEachEvaluateKey = (expression, eachExpression, sourceFile, sourceCode) => simplePropertyNode( | |||||
BINDING_EVALUATE_KEY, | |||||
compose( | |||||
e => toScopedFunction(e, sourceFile, sourceCode), | |||||
e => ({ | |||||
...eachExpression, | |||||
text: generateJavascript(e).code | |||||
}), | |||||
getEachValue | |||||
)(expression) | |||||
) | |||||
/** | |||||
* Get the each expression properties to create properly the template binding | |||||
* @param { DomBinding.Expression } eachExpression - original each expression data | |||||
* @param { string } sourceFile - original tag file | |||||
* @param { string } sourceCode - original tag source code | |||||
* @returns { Array } AST nodes that are needed to build an each binding | |||||
*/ | |||||
export function generateEachExpressionProperties(eachExpression, sourceFile, sourceCode) { | |||||
const ast = createASTFromExpression(eachExpression, sourceFile, sourceCode) | |||||
const body = ast.program.body | |||||
const firstNode = body[0] | |||||
if (!isExpressionStatement(firstNode)) { | |||||
panic(`The each directives supported should be of type "ExpressionStatement",you have provided a "${firstNode.type}"`) | |||||
} | |||||
const { expression } = firstNode | |||||
return [ | |||||
generateEachItemNameKey(expression), | |||||
generateEachIndexNameKey(expression), | |||||
generateEachEvaluateKey(expression, eachExpression, sourceFile, sourceCode) | |||||
] | |||||
} | |||||
/** | |||||
* Transform a RiotParser.Node.Tag into an each binding | |||||
* @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute | |||||
* @param { string } selectorAttribute - attribute needed to select the target node | |||||
* @param { string } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @returns { AST.Node } an each binding node | |||||
*/ | |||||
export default function createEachBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { | |||||
const [ifAttribute, eachAttribute, keyAttribute] = [ | |||||
findIfAttribute, | |||||
findEachAttribute, | |||||
findKeyAttribute | |||||
].map(f => f(sourceNode)) | |||||
const attributeOrNull = attribute => attribute ? toScopedFunction(getAttributeExpression(attribute), sourceFile, sourceCode) : nullNode() | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_TYPE_KEY, | |||||
builders.memberExpression( | |||||
builders.identifier(BINDING_TYPES), | |||||
builders.identifier(EACH_BINDING_TYPE), | |||||
false | |||||
), | |||||
), | |||||
simplePropertyNode(BINDING_GET_KEY_KEY, attributeOrNull(keyAttribute)), | |||||
simplePropertyNode(BINDING_CONDITION_KEY, attributeOrNull(ifAttribute)), | |||||
createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)), | |||||
...createSelectorProperties(selectorAttribute), | |||||
...compose(generateEachExpressionProperties, getAttributeExpression)(eachAttribute) | |||||
]) | |||||
} |
@ -0,0 +1,43 @@ | |||||
import { | |||||
BINDING_EVALUATE_KEY, | |||||
BINDING_TYPES, | |||||
BINDING_TYPE_KEY, | |||||
IF_BINDING_TYPE | |||||
} from '../constants' | |||||
import { | |||||
createSelectorProperties, | |||||
createTemplateProperty, | |||||
toScopedFunction | |||||
} from '../utils' | |||||
import {builders} from '../../../utils/build-types' | |||||
import {createNestedBindings} from '../builder' | |||||
import {findIfAttribute} from '../find' | |||||
import {simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
/** | |||||
* Transform a RiotParser.Node.Tag into an if binding | |||||
* @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute | |||||
* @param { string } selectorAttribute - attribute needed to select the target node | |||||
* @param { stiring } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @returns { AST.Node } an if binding node | |||||
*/ | |||||
export default function createIfBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { | |||||
const ifAttribute = findIfAttribute(sourceNode) | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_TYPE_KEY, | |||||
builders.memberExpression( | |||||
builders.identifier(BINDING_TYPES), | |||||
builders.identifier(IF_BINDING_TYPE), | |||||
false | |||||
), | |||||
), | |||||
simplePropertyNode( | |||||
BINDING_EVALUATE_KEY, | |||||
toScopedFunction(ifAttribute.expressions[0], sourceFile, sourceCode) | |||||
), | |||||
...createSelectorProperties(selectorAttribute), | |||||
createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)) | |||||
]) | |||||
} |
@ -0,0 +1,52 @@ | |||||
import {createAttributeExpressions, createExpression} from '../expressions/index' | |||||
import { | |||||
createSelectorProperties, | |||||
getChildrenNodes | |||||
} from '../utils' | |||||
import { hasExpressions, isTextNode } from '../checks' | |||||
import {BINDING_EXPRESSIONS_KEY} from '../constants' | |||||
import {builders} from '../../../utils/build-types' | |||||
import {simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
/** | |||||
* Create the text node expressions | |||||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @returns {Array} array containing all the text node expressions | |||||
*/ | |||||
function createTextNodeExpressions(sourceNode, sourceFile, sourceCode) { | |||||
const childrenNodes = getChildrenNodes(sourceNode) | |||||
return childrenNodes | |||||
.filter(isTextNode) | |||||
.filter(hasExpressions) | |||||
.map(node => createExpression( | |||||
node, | |||||
sourceFile, | |||||
sourceCode, | |||||
childrenNodes.indexOf(node), | |||||
sourceNode | |||||
)) | |||||
} | |||||
/** | |||||
* Add a simple binding to a riot parser node | |||||
* @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute | |||||
* @param { string } selectorAttribute - attribute needed to select the target node | |||||
* @param { string } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @returns { AST.Node } an each binding node | |||||
*/ | |||||
export default function createSimpleBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { | |||||
return builders.objectExpression([ | |||||
...createSelectorProperties(selectorAttribute), | |||||
simplePropertyNode( | |||||
BINDING_EXPRESSIONS_KEY, | |||||
builders.arrayExpression([ | |||||
...createTextNodeExpressions(sourceNode, sourceFile, sourceCode), | |||||
...createAttributeExpressions(sourceNode, sourceFile, sourceCode) | |||||
]) | |||||
) | |||||
]) | |||||
} |
@ -0,0 +1,38 @@ | |||||
import { | |||||
BINDING_NAME_KEY, | |||||
BINDING_TYPES, | |||||
BINDING_TYPE_KEY, | |||||
DEFAULT_SLOT_NAME, | |||||
NAME_ATTRIBUTE, | |||||
SLOT_BINDING_TYPE | |||||
} from '../constants' | |||||
import {builders} from '../../../utils/build-types' | |||||
import {createSelectorProperties} from '../utils' | |||||
import {findAttribute} from '../find' | |||||
import {simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
/** | |||||
* Transform a RiotParser.Node.Tag of type slot into a slot binding | |||||
* @param { RiotParser.Node.Tag } sourceNode - slot node | |||||
* @param { string } selectorAttribute - attribute needed to select the target node | |||||
* @returns { AST.Node } a slot binding node | |||||
*/ | |||||
export default function createSlotBinding(sourceNode, selectorAttribute) { | |||||
const slotNameAttribute = findAttribute(NAME_ATTRIBUTE, sourceNode) | |||||
const slotName = slotNameAttribute ? slotNameAttribute.value : DEFAULT_SLOT_NAME | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_TYPE_KEY, | |||||
builders.memberExpression( | |||||
builders.identifier(BINDING_TYPES), | |||||
builders.identifier(SLOT_BINDING_TYPE), | |||||
false | |||||
), | |||||
), | |||||
simplePropertyNode( | |||||
BINDING_NAME_KEY, | |||||
builders.literal(slotName) | |||||
), | |||||
...createSelectorProperties(selectorAttribute) | |||||
]) | |||||
} |
@ -0,0 +1,151 @@ | |||||
import { | |||||
BINDING_ATTRIBUTES_KEY, | |||||
BINDING_BINDINGS_KEY, | |||||
BINDING_EVALUATE_KEY, | |||||
BINDING_GET_COMPONENT_KEY, | |||||
BINDING_HTML_KEY, | |||||
BINDING_ID_KEY, | |||||
BINDING_SLOTS_KEY, | |||||
BINDING_TYPES, | |||||
BINDING_TYPE_KEY, | |||||
GET_COMPONENT_FN, | |||||
SLOT_ATTRIBUTE, | |||||
TAG_BINDING_TYPE | |||||
} from '../constants' | |||||
import { | |||||
cleanAttributes, | |||||
createRootNode, | |||||
createSelectorProperties, | |||||
getAttributesWithoutSelector, | |||||
getChildrenNodes, | |||||
getCustomNodeNameAsExpression, | |||||
getNodeAttributes, | |||||
toScopedFunction | |||||
} from '../utils' | |||||
import build from '../builder' | |||||
import {builders} from '../../../utils/build-types' | |||||
import compose from 'cumpa' | |||||
import {createExpression} from '../expressions/index' | |||||
import {simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
/** | |||||
* Find the slots in the current component and group them under the same id | |||||
* @param {RiotParser.Node.Tag} sourceNode - the custom tag | |||||
* @returns {Object} object containing all the slots grouped by name | |||||
*/ | |||||
function groupSlots(sourceNode) { | |||||
return getChildrenNodes(sourceNode).reduce((acc, node) => { | |||||
const slotAttribute = findSlotAttribute(node) | |||||
if (slotAttribute) { | |||||
acc[slotAttribute.value] = node | |||||
} else { | |||||
acc.default = createRootNode({ | |||||
nodes: [...getChildrenNodes(acc.default), node] | |||||
}) | |||||
} | |||||
return acc | |||||
}, { | |||||
default: null | |||||
}) | |||||
} | |||||
/** | |||||
* Create the slot entity to pass to the riot-dom bindings | |||||
* @param {string} id - slot id | |||||
* @param {RiotParser.Node.Tag} sourceNode - slot root node | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @returns {AST.Node} ast node containing the slot object properties | |||||
*/ | |||||
function buildSlot(id, sourceNode, sourceFile, sourceCode) { | |||||
const cloneNode = { | |||||
...sourceNode, | |||||
// avoid to render the slot attribute | |||||
attributes: getNodeAttributes(sourceNode).filter(attribute => attribute.name !== SLOT_ATTRIBUTE) | |||||
} | |||||
const [html, bindings] = build(cloneNode, sourceFile, sourceCode) | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_ID_KEY, builders.literal(id)), | |||||
simplePropertyNode(BINDING_HTML_KEY, builders.literal(html)), | |||||
simplePropertyNode(BINDING_BINDINGS_KEY, builders.arrayExpression(bindings)) | |||||
]) | |||||
} | |||||
/** | |||||
* Create the AST array containing the slots | |||||
* @param { RiotParser.Node.Tag } sourceNode - the custom tag | |||||
* @param { string } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @returns {AST.ArrayExpression} array containing the attributes to bind | |||||
*/ | |||||
function createSlotsArray(sourceNode, sourceFile, sourceCode) { | |||||
return builders.arrayExpression([ | |||||
...compose( | |||||
slots => slots.map(([key, value]) => buildSlot(key, value, sourceFile, sourceCode)), | |||||
slots => slots.filter(([,value]) => value), | |||||
Object.entries, | |||||
groupSlots | |||||
)(sourceNode) | |||||
]) | |||||
} | |||||
/** | |||||
* Create the AST array containing the attributes to bind to this node | |||||
* @param { RiotParser.Node.Tag } sourceNode - the custom tag | |||||
* @param { string } selectorAttribute - attribute needed to select the target node | |||||
* @param { string } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @returns {AST.ArrayExpression} array containing the slot objects | |||||
*/ | |||||
function createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) { | |||||
return builders.arrayExpression([ | |||||
...compose( | |||||
attributes => attributes.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)), | |||||
attributes => getAttributesWithoutSelector(attributes, selectorAttribute), // eslint-disable-line | |||||
cleanAttributes | |||||
)(sourceNode) | |||||
]) | |||||
} | |||||
/** | |||||
* Find the slot attribute if it exists | |||||
* @param {RiotParser.Node.Tag} sourceNode - the custom tag | |||||
* @returns {RiotParser.Node.Attr|undefined} the slot attribute found | |||||
*/ | |||||
function findSlotAttribute(sourceNode) { | |||||
return getNodeAttributes(sourceNode).find(attribute => attribute.name === SLOT_ATTRIBUTE) | |||||
} | |||||
/** | |||||
* Transform a RiotParser.Node.Tag into a tag binding | |||||
* @param { RiotParser.Node.Tag } sourceNode - the custom tag | |||||
* @param { string } selectorAttribute - attribute needed to select the target node | |||||
* @param { string } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @returns { AST.Node } tag binding node | |||||
*/ | |||||
export default function createTagBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_TYPE_KEY, | |||||
builders.memberExpression( | |||||
builders.identifier(BINDING_TYPES), | |||||
builders.identifier(TAG_BINDING_TYPE), | |||||
false | |||||
), | |||||
), | |||||
simplePropertyNode(BINDING_GET_COMPONENT_KEY, builders.identifier(GET_COMPONENT_FN)), | |||||
simplePropertyNode( | |||||
BINDING_EVALUATE_KEY, | |||||
toScopedFunction(getCustomNodeNameAsExpression(sourceNode), sourceFile, sourceCode) | |||||
), | |||||
simplePropertyNode(BINDING_SLOTS_KEY, createSlotsArray(sourceNode, sourceFile, sourceCode)), | |||||
simplePropertyNode( | |||||
BINDING_ATTRIBUTES_KEY, | |||||
createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) | |||||
), | |||||
...createSelectorProperties(selectorAttribute) | |||||
]) | |||||
} |
@ -0,0 +1,178 @@ | |||||
import { | |||||
cloneNodeWithoutSelectorAttribute, | |||||
closeTag, createBindingSelector, | |||||
createRootNode, | |||||
getChildrenNodes, | |||||
getNodeAttributes, | |||||
nodeToString | |||||
} from './utils' | |||||
import { | |||||
hasEachAttribute, hasIfAttribute, | |||||
hasItsOwnTemplate, | |||||
isCustomNode, | |||||
isRootNode, | |||||
isSlotNode, | |||||
isStaticNode, | |||||
isTagNode, | |||||
isTextNode, | |||||
isVoidNode | |||||
} from './checks' | |||||
import cloneDeep from '../../utils/clone-deep' | |||||
import eachBinding from './bindings/each' | |||||
import ifBinding from './bindings/if' | |||||
import panic from '../../utils/panic' | |||||
import simpleBinding from './bindings/simple' | |||||
import slotBinding from './bindings/slot' | |||||
import tagBinding from './bindings/tag' | |||||
const BuildingState = Object.freeze({ | |||||
html: [], | |||||
bindings: [], | |||||
parent: null | |||||
}) | |||||
/** | |||||
* Nodes having bindings should be cloned and new selector properties should be added to them | |||||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser | |||||
* @param {string} bindingsSelector - temporary string to identify the current node | |||||
* @returns {RiotParser.Node} the original node parsed having the new binding selector attribute | |||||
*/ | |||||
function createBindingsTag(sourceNode, bindingsSelector) { | |||||
if (!bindingsSelector) return sourceNode | |||||
return { | |||||
...sourceNode, | |||||
// inject the selector bindings into the node attributes | |||||
attributes: [{ | |||||
name: bindingsSelector, | |||||
value: bindingsSelector | |||||
}, ...getNodeAttributes(sourceNode)] | |||||
} | |||||
} | |||||
/** | |||||
* Create a generic dynamic node (text or tag) and generate its bindings | |||||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @param {BuildingState} state - state representing the current building tree state during the recursion | |||||
* @returns {Array} array containing the html output and bindings for the current node | |||||
*/ | |||||
function createDynamicNode(sourceNode, sourceFile, sourceCode, state) { | |||||
switch (true) { | |||||
case isTextNode(sourceNode): | |||||
// text nodes will not have any bindings | |||||
return [nodeToString(sourceNode), []] | |||||
default: | |||||
return createTagWithBindings(sourceNode, sourceFile, sourceCode, state) | |||||
} | |||||
} | |||||
/** | |||||
* Create only a dynamic tag node with generating a custom selector and its bindings | |||||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @param {BuildingState} state - state representing the current building tree state during the recursion | |||||
* @returns {Array} array containing the html output and bindings for the current node | |||||
*/ | |||||
function createTagWithBindings(sourceNode, sourceFile, sourceCode) { | |||||
const bindingsSelector = isRootNode(sourceNode) ? null : createBindingSelector() | |||||
const cloneNode = createBindingsTag(sourceNode, bindingsSelector) | |||||
const tagOpeningHTML = nodeToString(cloneNode) | |||||
switch(true) { | |||||
// EACH bindings have prio 1 | |||||
case hasEachAttribute(cloneNode): | |||||
return [tagOpeningHTML, [eachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] | |||||
// IF bindings have prio 2 | |||||
case hasIfAttribute(cloneNode): | |||||
return [tagOpeningHTML, [ifBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] | |||||
// TAG bindings have prio 3 | |||||
case isCustomNode(cloneNode): | |||||
return [tagOpeningHTML, [tagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] | |||||
// slot tag | |||||
case isSlotNode(cloneNode): | |||||
return [tagOpeningHTML, [slotBinding(cloneNode, bindingsSelector)]] | |||||
// this node has expressions bound to it | |||||
default: | |||||
return [tagOpeningHTML, [simpleBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] | |||||
} | |||||
} | |||||
/** | |||||
* Parse a node trying to extract its template and bindings | |||||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @param {BuildingState} state - state representing the current building tree state during the recursion | |||||
* @returns {Array} array containing the html output and bindings for the current node | |||||
*/ | |||||
function parseNode(sourceNode, sourceFile, sourceCode, state) { | |||||
// static nodes have no bindings | |||||
if (isStaticNode(sourceNode)) return [nodeToString(sourceNode), []] | |||||
return createDynamicNode(sourceNode, sourceFile, sourceCode, state) | |||||
} | |||||
/** | |||||
* Create the tag binding | |||||
* @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute | |||||
* @param { string } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @param { string } selector - binding selector | |||||
* @returns { Array } array with only the tag binding AST | |||||
*/ | |||||
export function createNestedBindings(sourceNode, sourceFile, sourceCode, selector) { | |||||
const mightBeARiotComponent = isCustomNode(sourceNode) | |||||
return mightBeARiotComponent ? [null, [ | |||||
tagBinding( | |||||
cloneNodeWithoutSelectorAttribute(sourceNode, selector), | |||||
null, | |||||
sourceFile, | |||||
sourceCode | |||||
)] | |||||
] : build(createRootNode(sourceNode), sourceFile, sourceCode) | |||||
} | |||||
/** | |||||
* Build the template and the bindings | |||||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @param {BuildingState} state - state representing the current building tree state during the recursion | |||||
* @returns {Array} array containing the html output and the dom bindings | |||||
*/ | |||||
export default function build( | |||||
sourceNode, | |||||
sourceFile, | |||||
sourceCode, | |||||
state | |||||
) { | |||||
if (!sourceNode) panic('Something went wrong with your tag DOM parsing, your tag template can\'t be created') | |||||
const [nodeHTML, nodeBindings] = parseNode(sourceNode, sourceFile, sourceCode, state) | |||||
const childrenNodes = getChildrenNodes(sourceNode) | |||||
const currentState = { ...cloneDeep(BuildingState), ...state } | |||||
// mutate the original arrays | |||||
currentState.html.push(...nodeHTML) | |||||
currentState.bindings.push(...nodeBindings) | |||||
// do recursion if | |||||
// this tag has children and it has no special directives bound to it | |||||
if (childrenNodes.length && !hasItsOwnTemplate(sourceNode)) { | |||||
childrenNodes.forEach(node => build(node, sourceFile, sourceCode, { parent: sourceNode, ...currentState })) | |||||
} | |||||
// close the tag if it's not a void one | |||||
if (isTagNode(sourceNode) && !isVoidNode(sourceNode)) { | |||||
currentState.html.push(closeTag(sourceNode)) | |||||
} | |||||
return [ | |||||
currentState.html.join(''), | |||||
currentState.bindings | |||||
] | |||||
} |
@ -0,0 +1,194 @@ | |||||
import { | |||||
IS_CUSTOM_NODE, | |||||
IS_SPREAD_ATTRIBUTE, | |||||
IS_VOID_NODE, | |||||
PROGRESS_TAG_NODE_NAME, | |||||
SLOT_TAG_NODE_NAME | |||||
} from './constants' | |||||
import { findEachAttribute, findIfAttribute, findIsAttribute, findKeyAttribute } from './find' | |||||
import { | |||||
getName, | |||||
getNodeAttributes | |||||
} from './utils' | |||||
import { isBrowserAPI, isBuiltinAPI, isNewExpression, isRaw } from '../../utils/ast-nodes-checks' | |||||
import compose from 'cumpa' | |||||
import { nodeTypes } from '@riotjs/parser' | |||||
import { types } from '../../utils/build-types' | |||||
/** | |||||
* True if the node has not expression set nor bindings directives | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true only if it's a static node that doesn't need bindings or expressions | |||||
*/ | |||||
export function isStaticNode(node) { | |||||
return [ | |||||
hasExpressions, | |||||
findEachAttribute, | |||||
findIfAttribute, | |||||
isCustomNode, | |||||
isSlotNode | |||||
].every(test => !test(node)) | |||||
} | |||||
/** | |||||
* Check if a node name is part of the browser or builtin javascript api or it belongs to the current scope | |||||
* @param { types.NodePath } path - containing the current node visited | |||||
* @returns {boolean} true if it's a global api variable | |||||
*/ | |||||
export function isGlobal({ scope, node }) { | |||||
return Boolean( | |||||
isRaw(node) || | |||||
isBuiltinAPI(node) || | |||||
isBrowserAPI(node) || | |||||
isNewExpression(node) || | |||||
isNodeInScope(scope, node), | |||||
) | |||||
} | |||||
/** | |||||
* Checks if the identifier of a given node exists in a scope | |||||
* @param {Scope} scope - scope where to search for the identifier | |||||
* @param {types.Node} node - node to search for the identifier | |||||
* @returns {boolean} true if the node identifier is defined in the given scope | |||||
*/ | |||||
function isNodeInScope(scope, node) { | |||||
const traverse = (isInScope = false) => { | |||||
types.visit(node, { | |||||
visitIdentifier(path) { | |||||
if (scope.lookup(getName(path.node))) { | |||||
isInScope = true | |||||
} | |||||
this.abort() | |||||
} | |||||
}) | |||||
return isInScope | |||||
} | |||||
return traverse() | |||||
} | |||||
/** | |||||
* True if the node has the isCustom attribute set | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true if either it's a riot component or a custom element | |||||
*/ | |||||
export function isCustomNode(node) { | |||||
return !!(node[IS_CUSTOM_NODE] || hasIsAttribute(node)) | |||||
} | |||||
/** | |||||
* True the node is <slot> | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true if it's a slot node | |||||
*/ | |||||
export function isSlotNode(node) { | |||||
return node.name === SLOT_TAG_NODE_NAME | |||||
} | |||||
/** | |||||
* True if the node has the isVoid attribute set | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true if the node is self closing | |||||
*/ | |||||
export function isVoidNode(node) { | |||||
return !!node[IS_VOID_NODE] | |||||
} | |||||
/** | |||||
* True if the riot parser did find a tag node | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true only for the tag nodes | |||||
*/ | |||||
export function isTagNode(node) { | |||||
return node.type === nodeTypes.TAG | |||||
} | |||||
/** | |||||
* True if the riot parser did find a text node | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true only for the text nodes | |||||
*/ | |||||
export function isTextNode(node) { | |||||
return node.type === nodeTypes.TEXT | |||||
} | |||||
/** | |||||
* True if the node parsed is the root one | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true only for the root nodes | |||||
*/ | |||||
export function isRootNode(node) { | |||||
return node.isRoot | |||||
} | |||||
/** | |||||
* True if the attribute parsed is of type spread one | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true if the attribute node is of type spread | |||||
*/ | |||||
export function isSpreadAttribute(node) { | |||||
return node[IS_SPREAD_ATTRIBUTE] | |||||
} | |||||
/** | |||||
* True if the node is an attribute and its name is "value" | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true only for value attribute nodes | |||||
*/ | |||||
export function isValueAttribute(node) { | |||||
return node.name === 'value' | |||||
} | |||||
/** | |||||
* True if the DOM node is a progress tag | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true for the progress tags | |||||
*/ | |||||
export function isProgressNode(node) { | |||||
return node.name === PROGRESS_TAG_NODE_NAME | |||||
} | |||||
/** | |||||
* True if the node is an attribute and a DOM handler | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true only for dom listener attribute nodes | |||||
*/ | |||||
export const isEventAttribute = (() => { | |||||
const EVENT_ATTR_RE = /^on/ | |||||
return node => EVENT_ATTR_RE.test(node.name) | |||||
})() | |||||
/** | |||||
* True if the node has expressions or expression attributes | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} ditto | |||||
*/ | |||||
export function hasExpressions(node) { | |||||
return !!( | |||||
node.expressions || | |||||
// has expression attributes | |||||
(getNodeAttributes(node).some(attribute => hasExpressions(attribute))) || | |||||
// has child text nodes with expressions | |||||
(node.nodes && node.nodes.some(node => isTextNode(node) && hasExpressions(node))) | |||||
) | |||||
} | |||||
/** | |||||
* True if the node is a directive having its own template | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {boolean} true only for the IF EACH and TAG bindings | |||||
*/ | |||||
export function hasItsOwnTemplate(node) { | |||||
return [ | |||||
findEachAttribute, | |||||
findIfAttribute, | |||||
isCustomNode | |||||
].some(test => test(node)) | |||||
} | |||||
export const hasIfAttribute = compose(Boolean, findIfAttribute) | |||||
export const hasEachAttribute = compose(Boolean, findEachAttribute) | |||||
export const hasIsAttribute = compose(Boolean, findIsAttribute) | |||||
export const hasKeyAttribute = compose(Boolean, findKeyAttribute) |
@ -0,0 +1,64 @@ | |||||
// import {IS_BOOLEAN,IS_CUSTOM,IS_RAW,IS_SPREAD,IS_VOID} from '@riotjs/parser/src/constants' | |||||
export const BINDING_TYPES = 'bindingTypes' | |||||
export const EACH_BINDING_TYPE = 'EACH' | |||||
export const IF_BINDING_TYPE = 'IF' | |||||
export const TAG_BINDING_TYPE = 'TAG' | |||||
export const SLOT_BINDING_TYPE = 'SLOT' | |||||
export const EXPRESSION_TYPES = 'expressionTypes' | |||||
export const ATTRIBUTE_EXPRESSION_TYPE = 'ATTRIBUTE' | |||||
export const VALUE_EXPRESSION_TYPE = 'VALUE' | |||||
export const TEXT_EXPRESSION_TYPE = 'TEXT' | |||||
export const EVENT_EXPRESSION_TYPE = 'EVENT' | |||||
export const TEMPLATE_FN = 'template' | |||||
export const SCOPE = 'scope' | |||||
export const GET_COMPONENT_FN = 'getComponent' | |||||
// keys needed to create the DOM bindings | |||||
export const BINDING_SELECTOR_KEY = 'selector' | |||||
export const BINDING_GET_COMPONENT_KEY = 'getComponent' | |||||
export const BINDING_TEMPLATE_KEY = 'template' | |||||
export const BINDING_TYPE_KEY = 'type' | |||||
export const BINDING_REDUNDANT_ATTRIBUTE_KEY = 'redundantAttribute' | |||||
export const BINDING_CONDITION_KEY = 'condition' | |||||
export const BINDING_ITEM_NAME_KEY = 'itemName' | |||||
export const BINDING_GET_KEY_KEY = 'getKey' | |||||
export const BINDING_INDEX_NAME_KEY = 'indexName' | |||||
export const BINDING_EVALUATE_KEY = 'evaluate' | |||||
export const BINDING_NAME_KEY = 'name' | |||||
export const BINDING_SLOTS_KEY = 'slots' | |||||
export const BINDING_EXPRESSIONS_KEY = 'expressions' | |||||
export const BINDING_CHILD_NODE_INDEX_KEY = 'childNodeIndex' | |||||
// slots keys | |||||
export const BINDING_BINDINGS_KEY = 'bindings' | |||||
export const BINDING_ID_KEY = 'id' | |||||
export const BINDING_HTML_KEY = 'html' | |||||
export const BINDING_ATTRIBUTES_KEY = 'attributes' | |||||
// DOM directives | |||||
export const IF_DIRECTIVE = 'if' | |||||
export const EACH_DIRECTIVE = 'each' | |||||
export const KEY_ATTRIBUTE = 'key' | |||||
export const SLOT_ATTRIBUTE = 'slot' | |||||
export const NAME_ATTRIBUTE = 'name' | |||||
export const IS_DIRECTIVE = 'is' | |||||
// Misc | |||||
export const DEFAULT_SLOT_NAME = 'default' | |||||
export const TEXT_NODE_EXPRESSION_PLACEHOLDER = '<!---->' | |||||
export const BINDING_SELECTOR_PREFIX = 'expr' | |||||
export const SLOT_TAG_NODE_NAME = 'slot' | |||||
export const PROGRESS_TAG_NODE_NAME = 'progress' | |||||
// Riot Parser constants | |||||
// TODO: import these values dynamically | |||||
export const IS_RAW_NODE = 'isRaw' | |||||
export const IS_VOID_NODE = 'isVoid' | |||||
export const IS_CUSTOM_NODE = 'isCustom' | |||||
export const IS_BOOLEAN_ATTRIBUTE = 'isBoolean' | |||||
export const IS_SPREAD_ATTRIBUTE = 'isSpread' | |||||
@ -0,0 +1,36 @@ | |||||
import { | |||||
ATTRIBUTE_EXPRESSION_TYPE, | |||||
BINDING_EVALUATE_KEY, | |||||
BINDING_NAME_KEY, | |||||
BINDING_TYPE_KEY, | |||||
EXPRESSION_TYPES | |||||
} from '../constants' | |||||
import {nullNode, simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
import {builders} from '../../../utils/build-types' | |||||
import {createAttributeEvaluationFunction} from '../utils' | |||||
import {isSpreadAttribute} from '../checks' | |||||
/** | |||||
* Create a simple attribute expression | |||||
* @param {RiotParser.Node.Attr} sourceNode - the custom tag | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @returns {AST.Node} object containing the expression binding keys | |||||
*/ | |||||
export default function createAttributeExpression(sourceNode, sourceFile, sourceCode) { | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_TYPE_KEY, | |||||
builders.memberExpression( | |||||
builders.identifier(EXPRESSION_TYPES), | |||||
builders.identifier(ATTRIBUTE_EXPRESSION_TYPE), | |||||
false | |||||
), | |||||
), | |||||
simplePropertyNode(BINDING_NAME_KEY, isSpreadAttribute(sourceNode) ? nullNode() : builders.literal(sourceNode.name)), | |||||
simplePropertyNode( | |||||
BINDING_EVALUATE_KEY, | |||||
createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) | |||||
) | |||||
]) | |||||
} |
@ -0,0 +1,34 @@ | |||||
import { | |||||
BINDING_EVALUATE_KEY, | |||||
BINDING_NAME_KEY, | |||||
BINDING_TYPE_KEY, | |||||
EVENT_EXPRESSION_TYPE, | |||||
EXPRESSION_TYPES | |||||
} from '../constants' | |||||
import {builders} from '../../../utils/build-types' | |||||
import {createAttributeEvaluationFunction} from '../utils' | |||||
import {simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
/** | |||||
* Create a simple event expression | |||||
* @param {RiotParser.Node.Attr} sourceNode - attribute containing the event handlers | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @returns {AST.Node} object containing the expression binding keys | |||||
*/ | |||||
export default function createEventExpression(sourceNode, sourceFile, sourceCode) { | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_TYPE_KEY, | |||||
builders.memberExpression( | |||||
builders.identifier(EXPRESSION_TYPES), | |||||
builders.identifier(EVENT_EXPRESSION_TYPE), | |||||
false | |||||
), | |||||
), | |||||
simplePropertyNode(BINDING_NAME_KEY, builders.literal(sourceNode.name)), | |||||
simplePropertyNode( | |||||
BINDING_EVALUATE_KEY, | |||||
createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) | |||||
) | |||||
]) | |||||
} |
@ -0,0 +1,34 @@ | |||||
import {isEventAttribute, isProgressNode, isTextNode, isValueAttribute} from '../checks' | |||||
import attributeExpression from './attribute' | |||||
import eventExpression from './event' | |||||
import {findDynamicAttributes} from '../find' | |||||
import {hasValueAttribute} from 'dom-nodes' | |||||
import textExpression from './text' | |||||
import valueExpression from './value' | |||||
export function createExpression(sourceNode, sourceFile, sourceCode, childNodeIndex, parentNode) { | |||||
switch (true) { | |||||
case isTextNode(sourceNode): | |||||
return textExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) | |||||
// progress nodes value attributes will be rendered as attributes | |||||
// see https://github.com/riot/compiler/issues/122 | |||||
case isValueAttribute(sourceNode) && hasValueAttribute(parentNode.name) && !isProgressNode(parentNode): | |||||
return valueExpression(sourceNode, sourceFile, sourceCode) | |||||
case isEventAttribute(sourceNode): | |||||
return eventExpression(sourceNode, sourceFile, sourceCode) | |||||
default: | |||||
return attributeExpression(sourceNode, sourceFile, sourceCode) | |||||
} | |||||
} | |||||
/** | |||||
* Create the attribute expressions | |||||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @returns {Array} array containing all the attribute expressions | |||||
*/ | |||||
export function createAttributeExpressions(sourceNode, sourceFile, sourceCode) { | |||||
return findDynamicAttributes(sourceNode) | |||||
.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)) | |||||
} |
@ -0,0 +1,90 @@ | |||||
import { | |||||
BINDING_CHILD_NODE_INDEX_KEY, | |||||
BINDING_EVALUATE_KEY, | |||||
BINDING_TYPE_KEY, | |||||
EXPRESSION_TYPES, | |||||
TEXT_EXPRESSION_TYPE | |||||
} from '../constants' | |||||
import {createArrayString, transformExpression, wrapASTInFunctionWithScope} from '../utils' | |||||
import {nullNode,simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
import {builders} from '../../../utils/build-types' | |||||
import {isLiteral} from '../../../utils/ast-nodes-checks' | |||||
import unescapeChar from '../../../utils/unescape-char' | |||||
/** | |||||
* Generate the pure immutable string chunks from a RiotParser.Node.Text | |||||
* @param {RiotParser.Node.Text} node - riot parser text node | |||||
* @param {string} sourceCode sourceCode - source code | |||||
* @returns {Array} array containing the immutable string chunks | |||||
*/ | |||||
function generateLiteralStringChunksFromNode(node, sourceCode) { | |||||
return node.expressions.reduce((chunks, expression, index) => { | |||||
const start = index ? node.expressions[index - 1].end : node.start | |||||
chunks.push(sourceCode.substring(start, expression.start)) | |||||
// add the tail to the string | |||||
if (index === node.expressions.length - 1) | |||||
chunks.push(sourceCode.substring(expression.end, node.end)) | |||||
return chunks | |||||
}, []).map(str => node.unescape ? unescapeChar(str, node.unescape) : str) | |||||
} | |||||
/** | |||||
* Simple bindings might contain multiple expressions like for example: "{foo} and {bar}" | |||||
* This helper aims to merge them in a template literal if it's necessary | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @param {string} sourceFile - original tag file | |||||
* @param {string} sourceCode - original tag source code | |||||
* @returns { Object } a template literal expression object | |||||
*/ | |||||
export function mergeNodeExpressions(node, sourceFile, sourceCode) { | |||||
if (node.parts.length === 1) | |||||
return transformExpression(node.expressions[0], sourceFile, sourceCode) | |||||
const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode) | |||||
const stringsArray = pureStringChunks.reduce((acc, str, index) => { | |||||
const expr = node.expressions[index] | |||||
return [ | |||||
...acc, | |||||
builders.literal(str), | |||||
expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode() | |||||
] | |||||
}, []) | |||||
// filter the empty literal expressions | |||||
.filter(expr => !isLiteral(expr) || expr.value) | |||||
return createArrayString(stringsArray) | |||||
} | |||||
/** | |||||
* Create a text expression | |||||
* @param {RiotParser.Node.Text} sourceNode - text node to parse | |||||
* @param {string} sourceFile - source file path | |||||
* @param {string} sourceCode - original source | |||||
* @param {number} childNodeIndex - position of the child text node in its parent children nodes | |||||
* @returns {AST.Node} object containing the expression binding keys | |||||
*/ | |||||
export default function createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) { | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_TYPE_KEY, | |||||
builders.memberExpression( | |||||
builders.identifier(EXPRESSION_TYPES), | |||||
builders.identifier(TEXT_EXPRESSION_TYPE), | |||||
false | |||||
), | |||||
), | |||||
simplePropertyNode( | |||||
BINDING_CHILD_NODE_INDEX_KEY, | |||||
builders.literal(childNodeIndex) | |||||
), | |||||
simplePropertyNode( | |||||
BINDING_EVALUATE_KEY, | |||||
wrapASTInFunctionWithScope( | |||||
mergeNodeExpressions(sourceNode, sourceFile, sourceCode) | |||||
) | |||||
) | |||||
]) | |||||
} |
@ -0,0 +1,25 @@ | |||||
import { | |||||
BINDING_EVALUATE_KEY, | |||||
BINDING_TYPE_KEY, | |||||
EXPRESSION_TYPES, | |||||
VALUE_EXPRESSION_TYPE | |||||
} from '../constants' | |||||
import {builders} from '../../../utils/build-types' | |||||
import {createAttributeEvaluationFunction} from '../utils' | |||||
import {simplePropertyNode} from '../../../utils/custom-ast-nodes' | |||||
export default function createValueExpression(sourceNode, sourceFile, sourceCode) { | |||||
return builders.objectExpression([ | |||||
simplePropertyNode(BINDING_TYPE_KEY, | |||||
builders.memberExpression( | |||||
builders.identifier(EXPRESSION_TYPES), | |||||
builders.identifier(VALUE_EXPRESSION_TYPE), | |||||
false | |||||
), | |||||
), | |||||
simplePropertyNode( | |||||
BINDING_EVALUATE_KEY, | |||||
createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) | |||||
) | |||||
]) | |||||
} |
@ -0,0 +1,47 @@ | |||||
import { EACH_DIRECTIVE, IF_DIRECTIVE, IS_DIRECTIVE, KEY_ATTRIBUTE } from './constants' | |||||
import { getName, getNodeAttributes } from './utils' | |||||
import { hasExpressions } from './checks' | |||||
/** | |||||
* Find the attribute node | |||||
* @param { string } name - name of the attribute we want to find | |||||
* @param { riotParser.nodeTypes.TAG } node - a tag node | |||||
* @returns { riotParser.nodeTypes.ATTR } attribute node | |||||
*/ | |||||
export function findAttribute(name, node) { | |||||
return node.attributes && node.attributes.find(attr => getName(attr) === name) | |||||
} | |||||
export function findIfAttribute(node) { | |||||
return findAttribute(IF_DIRECTIVE, node) | |||||
} | |||||
export function findEachAttribute(node) { | |||||
return findAttribute(EACH_DIRECTIVE, node) | |||||
} | |||||
export function findKeyAttribute(node) { | |||||
return findAttribute(KEY_ATTRIBUTE, node) | |||||
} | |||||
export function findIsAttribute(node) { | |||||
return findAttribute(IS_DIRECTIVE, node) | |||||
} | |||||
/** | |||||
* Find all the node attributes that are not expressions | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {Array} list of all the static attributes | |||||
*/ | |||||
export function findStaticAttributes(node) { | |||||
return getNodeAttributes(node).filter(attribute => !hasExpressions(attribute)) | |||||
} | |||||
/** | |||||
* Find all the node attributes that have expressions | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {Array} list of all the dynamic attributes | |||||
*/ | |||||
export function findDynamicAttributes(node) { | |||||
return getNodeAttributes(node).filter(hasExpressions) | |||||
} |
@ -0,0 +1,74 @@ | |||||
import {BINDING_TYPES, EXPRESSION_TYPES, GET_COMPONENT_FN, TEMPLATE_FN} from './constants' | |||||
import {builders, types} from '../../utils/build-types' | |||||
import {callTemplateFunction, createRootNode} from './utils' | |||||
import {TAG_TEMPLATE_PROPERTY} from '../../constants' | |||||
import build from './builder' | |||||
const templateFunctionArguments = [ | |||||
TEMPLATE_FN, | |||||
EXPRESSION_TYPES, | |||||
BINDING_TYPES, | |||||
GET_COMPONENT_FN | |||||
].map(builders.identifier) | |||||
/** | |||||
* Create the content of the template function | |||||
* @param { RiotParser.Node } sourceNode - node generated by the riot compiler | |||||
* @param { string } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @returns {AST.BlockStatement} the content of the template function | |||||
*/ | |||||
function createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) { | |||||
return builders.blockStatement([ | |||||
builders.returnStatement( | |||||
callTemplateFunction( | |||||
...build( | |||||
createRootNode(sourceNode), | |||||
sourceFile, | |||||
sourceCode | |||||
) | |||||
) | |||||
) | |||||
]) | |||||
} | |||||
/** | |||||
* Extend the AST adding the new template property containing our template call to render the component | |||||
* @param { Object } ast - current output ast | |||||
* @param { string } sourceFile - source file path | |||||
* @param { string } sourceCode - original source | |||||
* @param { RiotParser.Node } sourceNode - node generated by the riot compiler | |||||
* @returns { Object } the output ast having the "template" key | |||||
*/ | |||||
function extendTemplateProperty(ast, sourceFile, sourceCode, sourceNode) { | |||||
types.visit(ast, { | |||||
visitProperty(path) { | |||||
if (path.value.key.value === TAG_TEMPLATE_PROPERTY) { | |||||
path.value.value = builders.functionExpression( | |||||
null, | |||||
templateFunctionArguments, | |||||
createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) | |||||
) | |||||
return false | |||||
} | |||||
this.traverse(path) | |||||
} | |||||
}) | |||||
return ast | |||||
} | |||||
/** | |||||
* Generate the component template logic | |||||
* @param { RiotParser.Node } sourceNode - node generated by the riot compiler | |||||
* @param { string } source - original component source code | |||||
* @param { Object } meta - compilation meta information | |||||
* @param { AST } ast - current AST output | |||||
* @returns { AST } the AST generated | |||||
*/ | |||||
export default function template(sourceNode, source, meta, ast) { | |||||
const { options } = meta | |||||
return extendTemplateProperty(ast, options.file, source, sourceNode) | |||||
} |
@ -0,0 +1,486 @@ | |||||
import { | |||||
BINDING_REDUNDANT_ATTRIBUTE_KEY, | |||||
BINDING_SELECTOR_KEY, | |||||
BINDING_SELECTOR_PREFIX, | |||||
BINDING_TEMPLATE_KEY, | |||||
EACH_DIRECTIVE, | |||||
IF_DIRECTIVE, | |||||
IS_BOOLEAN_ATTRIBUTE, | |||||
IS_DIRECTIVE, | |||||
KEY_ATTRIBUTE, | |||||
SCOPE, | |||||
SLOT_ATTRIBUTE, | |||||
TEMPLATE_FN, | |||||
TEXT_NODE_EXPRESSION_PLACEHOLDER | |||||
} from './constants' | |||||
import { builders, types } from '../../utils/build-types' | |||||
import { findIsAttribute, findStaticAttributes } from './find' | |||||
import { hasExpressions, isGlobal, isTagNode, isTextNode, isVoidNode } from './checks' | |||||
import { isBinaryExpression, isIdentifier, isLiteral, isThisExpression } from '../../utils/ast-nodes-checks' | |||||
import { nullNode, simplePropertyNode } from '../../utils/custom-ast-nodes' | |||||
import addLinesOffset from '../../utils/add-lines-offset' | |||||
import compose from 'cumpa' | |||||
import generateAST from '../../utils/generate-ast' | |||||
import unescapeChar from '../../utils/unescape-char' | |||||
const scope = builders.identifier(SCOPE) | |||||
export const getName = node => node && node.name ? node.name : node | |||||
/** | |||||
* Replace the path scope with a member Expression | |||||
* @param { types.NodePath } path - containing the current node visited | |||||
* @param { types.Node } property - node we want to prefix with the scope identifier | |||||
* @returns {undefined} this is a void function | |||||
*/ | |||||
function replacePathScope(path, property) { | |||||
path.replace(builders.memberExpression( | |||||
scope, | |||||
property, | |||||
false | |||||
)) | |||||
} | |||||
/** | |||||
* Change the nodes scope adding the `scope` prefix | |||||
* @param { types.NodePath } path - containing the current node visited | |||||
* @returns { boolean } return false if we want to stop the tree traversal | |||||
* @context { types.visit } | |||||
*/ | |||||
function updateNodeScope(path) { | |||||
if (!isGlobal(path)) { | |||||
replacePathScope(path, path.node) | |||||
return false | |||||
} | |||||
this.traverse(path) | |||||
} | |||||
/** | |||||
* Change the scope of the member expressions | |||||
* @param { types.NodePath } path - containing the current node visited | |||||
* @returns { boolean } return always false because we want to check only the first node object | |||||
*/ | |||||
function visitMemberExpression(path) { | |||||
if (!isGlobal(path) && !isGlobal({ node: path.node.object, scope: path.scope })) { | |||||
if (path.value.computed) { | |||||
this.traverse(path) | |||||
} else if (isBinaryExpression(path.node.object) || path.node.object.computed) { | |||||
this.traverse(path.get('object')) | |||||
} else if (!path.node.object.callee) { | |||||
replacePathScope(path, isThisExpression(path.node.object) ? path.node.property : path.node) | |||||
} else { | |||||
this.traverse(path.get('object')) | |||||
} | |||||
} | |||||
return false | |||||
} | |||||
/** | |||||
* Objects properties should be handled a bit differently from the Identifier | |||||
* @param { types.NodePath } path - containing the current node visited | |||||
* @returns { boolean } return false if we want to stop the tree traversal | |||||
*/ | |||||
function visitProperty(path) { | |||||
const value = path.node.value | |||||
if (isIdentifier(value)) { | |||||
updateNodeScope(path.get('value')) | |||||
} else { | |||||
this.traverse(path.get('value')) | |||||
} | |||||
return false | |||||
} | |||||
/** | |||||
* The this expressions should be replaced with the scope | |||||
* @param { types.NodePath } path - containing the current node visited | |||||
* @returns { boolean|undefined } return false if we want to stop the tree traversal | |||||
*/ | |||||
function visitThisExpression(path) { | |||||
path.replace(scope) | |||||
this.traverse(path) | |||||
} | |||||
/** | |||||
* Update the scope of the global nodes | |||||
* @param { Object } ast - ast program | |||||
* @returns { Object } the ast program with all the global nodes updated | |||||
*/ | |||||
export function updateNodesScope(ast) { | |||||
const ignorePath = () => false | |||||
types.visit(ast, { | |||||
visitIdentifier: updateNodeScope, | |||||
visitMemberExpression, | |||||
visitProperty, | |||||
visitThisExpression, | |||||
visitClassExpression: ignorePath | |||||
}) | |||||
return ast | |||||
} | |||||
/** | |||||
* Convert any expression to an AST tree | |||||
* @param { Object } expression - expression parsed by the riot parser | |||||
* @param { string } sourceFile - original tag file | |||||
* @param { string } sourceCode - original tag source code | |||||
* @returns { Object } the ast generated | |||||
*/ | |||||
export function createASTFromExpression(expression, sourceFile, sourceCode) { | |||||
const code = sourceFile ? | |||||
addLinesOffset(expression.text, sourceCode, expression) : | |||||
expression.text | |||||
return generateAST(`(${code})`, { | |||||
sourceFileName: sourceFile | |||||
}) | |||||
} | |||||
/** | |||||
* Create the bindings template property | |||||
* @param {Array} args - arguments to pass to the template function | |||||
* @returns {ASTNode} a binding template key | |||||
*/ | |||||
export function createTemplateProperty(args) { | |||||
return simplePropertyNode( | |||||
BINDING_TEMPLATE_KEY, | |||||
args ? callTemplateFunction(...args) : nullNode() | |||||
) | |||||
} | |||||
/** | |||||
* Try to get the expression of an attribute node | |||||
* @param { RiotParser.Node.Attribute } attribute - riot parser attribute node | |||||
* @returns { RiotParser.Node.Expression } attribute expression value | |||||
*/ | |||||
export function getAttributeExpression(attribute) { | |||||
return attribute.expressions ? attribute.expressions[0] : { | |||||
// if no expression was found try to typecast the attribute value | |||||
...attribute, | |||||
text: attribute.value | |||||
} | |||||
} | |||||
/** | |||||
* Wrap the ast generated in a function call providing the scope argument | |||||
* @param {Object} ast - function body | |||||
* @returns {FunctionExpresion} function having the scope argument injected | |||||
*/ | |||||
export function wrapASTInFunctionWithScope(ast) { | |||||
return builders.functionExpression( | |||||
null, | |||||
[scope], | |||||
builders.blockStatement([builders.returnStatement( | |||||
ast | |||||
)]) | |||||
) | |||||
} | |||||
/** | |||||
* Convert any parser option to a valid template one | |||||
* @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser | |||||
* @param { string } sourceFile - original tag file | |||||
* @param { string } sourceCode - original tag source code | |||||
* @returns { Object } a FunctionExpression object | |||||
* | |||||
* @example | |||||
* toScopedFunction('foo + bar') // scope.foo + scope.bar | |||||
* | |||||
* @example | |||||
* toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar | |||||
*/ | |||||
export function toScopedFunction(expression, sourceFile, sourceCode) { | |||||
return compose( | |||||
wrapASTInFunctionWithScope, | |||||
transformExpression, | |||||
)(expression, sourceFile, sourceCode) | |||||
} | |||||
/** | |||||
* Transform an expression node updating its global scope | |||||
* @param {RiotParser.Node.Expr} expression - riot parser expression node | |||||
* @param {string} sourceFile - source file | |||||
* @param {string} sourceCode - source code | |||||
* @returns {ASTExpression} ast expression generated from the riot parser expression node | |||||
*/ | |||||
export function transformExpression(expression, sourceFile, sourceCode) { | |||||
return compose( | |||||
getExpressionAST, | |||||
updateNodesScope, | |||||
createASTFromExpression | |||||
)(expression, sourceFile, sourceCode) | |||||
} | |||||
/** | |||||
* Get the parsed AST expression of riot expression node | |||||
* @param {AST.Program} sourceAST - raw node parsed | |||||
* @returns {AST.Expression} program expression output | |||||
*/ | |||||
export function getExpressionAST(sourceAST) { | |||||
const astBody = sourceAST.program.body | |||||
return astBody[0] ? astBody[0].expression : astBody | |||||
} | |||||
/** | |||||
* Create the template call function | |||||
* @param {Array|string|Node.Literal} template - template string | |||||
* @param {Array<AST.Nodes>} bindings - template bindings provided as AST nodes | |||||
* @returns {Node.CallExpression} template call expression | |||||
*/ | |||||
export function callTemplateFunction(template, bindings) { | |||||
return builders.callExpression(builders.identifier(TEMPLATE_FN), [ | |||||
template ? builders.literal(template) : nullNode(), | |||||
bindings ? builders.arrayExpression(bindings) : nullNode() | |||||
]) | |||||
} | |||||
/** | |||||
* Convert any DOM attribute into a valid DOM selector useful for the querySelector API | |||||
* @param { string } attributeName - name of the attribute to query | |||||
* @returns { string } the attribute transformed to a query selector | |||||
*/ | |||||
export const attributeNameToDOMQuerySelector = attributeName => `[${attributeName}]` | |||||
/** | |||||
* Create the properties to query a DOM node | |||||
* @param { string } attributeName - attribute name needed to identify a DOM node | |||||
* @returns { Array<AST.Node> } array containing the selector properties needed for the binding | |||||
*/ | |||||
export function createSelectorProperties(attributeName) { | |||||
return attributeName ? [ | |||||
simplePropertyNode(BINDING_REDUNDANT_ATTRIBUTE_KEY, builders.literal(attributeName)), | |||||
simplePropertyNode(BINDING_SELECTOR_KEY, | |||||
compose(builders.literal, attributeNameToDOMQuerySelector)(attributeName) | |||||
) | |||||
] : [] | |||||
} | |||||
/** | |||||
* Clone the node filtering out the selector attribute from the attributes list | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @param {string} selectorAttribute - name of the selector attribute to filter out | |||||
* @returns {RiotParser.Node} the node with the attribute cleaned up | |||||
*/ | |||||
export function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) { | |||||
return { | |||||
...node, | |||||
attributes: getAttributesWithoutSelector(getNodeAttributes(node), selectorAttribute) | |||||
} | |||||
} | |||||
/** | |||||
* Get the node attributes without the selector one | |||||
* @param {Array<RiotParser.Attr>} attributes - attributes list | |||||
* @param {string} selectorAttribute - name of the selector attribute to filter out | |||||
* @returns {Array<RiotParser.Attr>} filtered attributes | |||||
*/ | |||||
export function getAttributesWithoutSelector(attributes, selectorAttribute) { | |||||
if (selectorAttribute) | |||||
return attributes.filter(attribute => attribute.name !== selectorAttribute) | |||||
return attributes | |||||
} | |||||
/** | |||||
* Clean binding or custom attributes | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {Array<RiotParser.Node.Attr>} only the attributes that are not bindings or directives | |||||
*/ | |||||
export function cleanAttributes(node) { | |||||
return getNodeAttributes(node).filter(attribute => ![ | |||||
IF_DIRECTIVE, | |||||
EACH_DIRECTIVE, | |||||
KEY_ATTRIBUTE, | |||||
SLOT_ATTRIBUTE, | |||||
IS_DIRECTIVE | |||||
].includes(attribute.name)) | |||||
} | |||||
/** | |||||
* Create a root node proxing only its nodes and attributes | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {RiotParser.Node} root node | |||||
*/ | |||||
export function createRootNode(node) { | |||||
return { | |||||
nodes: getChildrenNodes(node), | |||||
isRoot: true, | |||||
// root nodes shuold't have directives | |||||
attributes: cleanAttributes(node) | |||||
} | |||||
} | |||||
/** | |||||
* Get all the child nodes of a RiotParser.Node | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {Array<RiotParser.Node>} all the child nodes found | |||||
*/ | |||||
export function getChildrenNodes(node) { | |||||
return node && node.nodes ? node.nodes : [] | |||||
} | |||||
/** | |||||
* Get all the attributes of a riot parser node | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {Array<RiotParser.Node.Attribute>} all the attributes find | |||||
*/ | |||||
export function getNodeAttributes(node) { | |||||
return node.attributes ? node.attributes : [] | |||||
} | |||||
/** | |||||
* Get the name of a custom node transforming it into an expression node | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {RiotParser.Node.Attr} the node name as expression attribute | |||||
*/ | |||||
export function getCustomNodeNameAsExpression(node) { | |||||
const isAttribute = findIsAttribute(node) | |||||
const toRawString = val => `'${val}'` | |||||
if (isAttribute) { | |||||
return isAttribute.expressions ? isAttribute.expressions[0] : { | |||||
...isAttribute, | |||||
text: toRawString(isAttribute.value) | |||||
} | |||||
} | |||||
return { ...node, text: toRawString(getName(node)) } | |||||
} | |||||
/** | |||||
* Convert all the node static attributes to strings | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {string} all the node static concatenated as string | |||||
*/ | |||||
export function staticAttributesToString(node) { | |||||
return findStaticAttributes(node) | |||||
.map(attribute => attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value ? | |||||
attribute.name : | |||||
`${attribute.name}="${unescapeNode(attribute, 'value').value}"` | |||||
).join(' ') | |||||
} | |||||
/** | |||||
* Make sure that node escaped chars will be unescaped | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @param {string} key - key property to unescape | |||||
* @returns {RiotParser.Node} node with the text property unescaped | |||||
*/ | |||||
export function unescapeNode(node, key) { | |||||
if (node.unescape) { | |||||
return { | |||||
...node, | |||||
[key]: unescapeChar(node[key], node.unescape) | |||||
} | |||||
} | |||||
return node | |||||
} | |||||
/** | |||||
* Convert a riot parser opening node into a string | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {string} the node as string | |||||
*/ | |||||
export function nodeToString(node) { | |||||
const attributes = staticAttributesToString(node) | |||||
switch(true) { | |||||
case isTagNode(node): | |||||
return `<${node.name}${attributes ? ` ${attributes}` : ''}${isVoidNode(node) ? '/' : ''}>` | |||||
case isTextNode(node): | |||||
return hasExpressions(node) ? TEXT_NODE_EXPRESSION_PLACEHOLDER : unescapeNode(node, 'text').text | |||||
default: | |||||
return '' | |||||
} | |||||
} | |||||
/** | |||||
* Close an html node | |||||
* @param {RiotParser.Node} node - riot parser node | |||||
* @returns {string} the closing tag of the html tag node passed to this function | |||||
*/ | |||||
export function closeTag(node) { | |||||
return node.name ? `</${node.name}>` : '' | |||||
} | |||||
/** | |||||
* Create a strings array with the `join` call to transform it into a string | |||||
* @param {Array} stringsArray - array containing all the strings to concatenate | |||||
* @returns {AST.CallExpression} array with a `join` call | |||||
*/ | |||||
export function createArrayString(stringsArray) { | |||||
return builders.callExpression( | |||||
builders.memberExpression( | |||||
builders.arrayExpression(stringsArray), | |||||
builders.identifier('join'), | |||||
false | |||||
), | |||||
[builders.literal('')], | |||||
) | |||||
} | |||||
/** | |||||
* Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}"" | |||||
* This helper aims to merge them in a template literal if it's necessary | |||||
* @param {RiotParser.Attr} node - riot parser node | |||||
* @param {string} sourceFile - original tag file | |||||
* @param {string} sourceCode - original tag source code | |||||
* @returns { Object } a template literal expression object | |||||
*/ | |||||
export function mergeAttributeExpressions(node, sourceFile, sourceCode) { | |||||
if (!node.parts || node.parts.length === 1) { | |||||
return transformExpression(node.expressions[0], sourceFile, sourceCode) | |||||
} | |||||
const stringsArray = [ | |||||
...node.parts.reduce((acc, str) => { | |||||
const expression = node.expressions.find(e => e.text.trim() === str) | |||||
return [ | |||||
...acc, | |||||
expression ? transformExpression(expression, sourceFile, sourceCode) : builders.literal(str) | |||||
] | |||||
}, []) | |||||
].filter(expr => !isLiteral(expr) || expr.value) | |||||
return createArrayString(stringsArray) | |||||
} | |||||
/** | |||||
* Create a selector that will be used to find the node via dom-bindings | |||||
* @param {number} id - temporary variable that will be increased anytime this function will be called | |||||
* @returns {string} selector attribute needed to bind a riot expression | |||||
*/ | |||||
export const createBindingSelector = (function createSelector(id = 0) { | |||||
return () => `${BINDING_SELECTOR_PREFIX}${id++}` | |||||
}()) | |||||
/** | |||||
* Create an attribute evaluation function | |||||
* @param {RiotParser.Attr} sourceNode - riot parser node | |||||
* @param {string} sourceFile - original tag file | |||||
* @param {string} sourceCode - original tag source code | |||||
* @returns { AST.Node } an AST function expression to evaluate the attribute value | |||||
*/ | |||||
export function createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) { | |||||
return hasExpressions(sourceNode) ? | |||||
// dynamic attribute | |||||
wrapASTInFunctionWithScope(mergeAttributeExpressions(sourceNode, sourceFile, sourceCode)) : | |||||
// static attribute | |||||
builders.functionExpression( | |||||
null, | |||||
[], | |||||
builders.blockStatement([ | |||||
builders.returnStatement(builders.literal(sourceNode.value || true)) | |||||
]), | |||||
) | |||||
} |
@ -0,0 +1,156 @@ | |||||
import { TAG_CSS_PROPERTY, TAG_LOGIC_PROPERTY, TAG_NAME_PROPERTY, TAG_TEMPLATE_PROPERTY } from './constants' | |||||
import { nullNode, simplePropertyNode } from './utils/custom-ast-nodes' | |||||
import { register as registerPostproc, execute as runPostprocessors } from './postprocessors' | |||||
import { register as registerPreproc, execute as runPreprocessor } from './preprocessors' | |||||
import {builders} from './utils/build-types' | |||||
import compose from 'cumpa' | |||||
import cssGenerator from './generators/css/index' | |||||
import curry from 'curri' | |||||
import generateJavascript from './utils/generate-javascript' | |||||
import isEmptySourcemap from './utils/is-empty-sourcemap' | |||||
import javascriptGenerator from './generators/javascript/index' | |||||
import riotParser from '@riotjs/parser' | |||||
import sourcemapAsJSON from './utils/sourcemap-as-json' | |||||
import templateGenerator from './generators/template/index' | |||||
const DEFAULT_OPTIONS = { | |||||
template: 'default', | |||||
file: '[unknown-source-file]', | |||||
scopedCss: true | |||||
} | |||||
/** | |||||
* Create the initial AST | |||||
* @param {string} tagName - the name of the component we have compiled | |||||
* @returns { AST } the initial AST | |||||
* | |||||
* @example | |||||
* // the output represents the following string in AST | |||||
*/ | |||||
export function createInitialInput({tagName}) { | |||||
/* | |||||
generates | |||||
export default { | |||||
${TAG_CSS_PROPERTY}: null, | |||||
${TAG_LOGIC_PROPERTY}: null, | |||||
${TAG_TEMPLATE_PROPERTY}: null | |||||
} | |||||
*/ | |||||
return builders.program([ | |||||
builders.exportDefaultDeclaration( | |||||
builders.objectExpression([ | |||||
simplePropertyNode(TAG_CSS_PROPERTY, nullNode()), | |||||
simplePropertyNode(TAG_LOGIC_PROPERTY, nullNode()), | |||||
simplePropertyNode(TAG_TEMPLATE_PROPERTY, nullNode()), | |||||
simplePropertyNode(TAG_NAME_PROPERTY, builders.literal(tagName)) | |||||
]) | |||||
)] | |||||
) | |||||
} | |||||
/** | |||||
* Make sure the input sourcemap is valid otherwise we ignore it | |||||
* @param {SourceMapGenerator} map - preprocessor source map | |||||
* @returns {Object} sourcemap as json or nothing | |||||
*/ | |||||
function normaliseInputSourceMap(map) { | |||||
const inputSourceMap = sourcemapAsJSON(map) | |||||
return isEmptySourcemap(inputSourceMap) ? null : inputSourceMap | |||||
} | |||||
/** | |||||
* Override the sourcemap content making sure it will always contain the tag source code | |||||
* @param {Object} map - sourcemap as json | |||||
* @param {string} source - component source code | |||||
* @returns {Object} original source map with the "sourcesContent" property overriden | |||||
*/ | |||||
function overrideSourcemapContent(map, source) { | |||||
return { | |||||
...map, | |||||
sourcesContent: [source] | |||||
} | |||||
} | |||||
/** | |||||
* Create the compilation meta object | |||||
* @param { string } source - source code of the tag we will need to compile | |||||
* @param { string } options - compiling options | |||||
* @returns {Object} meta object | |||||
*/ | |||||
function createMeta(source, options) { | |||||
return { | |||||
tagName: null, | |||||
fragments: null, | |||||
options: { | |||||
...DEFAULT_OPTIONS, | |||||
...options | |||||
}, | |||||
source | |||||
} | |||||
} | |||||
/** | |||||
* Generate the output code source together with the sourcemap | |||||
* @param { string } source - source code of the tag we will need to compile | |||||
* @param { string } opts - compiling options | |||||
* @returns { Output } object containing output code and source map | |||||
*/ | |||||
export function compile(source, opts = {}) { | |||||
const meta = createMeta(source, opts) | |||||
const {options} = meta | |||||
const { code, map } = runPreprocessor('template', options.template, meta, source) | |||||
const { template, css, javascript } = riotParser(options).parse(code).output | |||||
// extend the meta object with the result of the parsing | |||||
Object.assign(meta, { | |||||
tagName: template.name, | |||||
fragments: { template, css, javascript } | |||||
}) | |||||
return compose( | |||||
result => ({ ...result, meta }), | |||||
result => runPostprocessors(result, meta), | |||||
result => ({ | |||||
...result, | |||||
map: overrideSourcemapContent(result.map, source) | |||||
}), | |||||
ast => meta.ast = ast && generateJavascript(ast, { | |||||
sourceMapName: `${options.file}.map`, | |||||
inputSourceMap: normaliseInputSourceMap(map) | |||||
}), | |||||
hookGenerator(templateGenerator, template, code, meta), | |||||
hookGenerator(javascriptGenerator, javascript, code, meta), | |||||
hookGenerator(cssGenerator, css, code, meta), | |||||
)(createInitialInput(meta)) | |||||
} | |||||
/** | |||||
* Prepare the riot parser node transformers | |||||
* @param { Function } transformer - transformer function | |||||
* @param { Object } sourceNode - riot parser node | |||||
* @param { string } source - component source code | |||||
* @param { Object } meta - compilation meta information | |||||
* @returns { Promise<Output> } object containing output code and source map | |||||
*/ | |||||
function hookGenerator(transformer, sourceNode, source, meta) { | |||||
if ( | |||||
// filter missing nodes | |||||
!sourceNode || | |||||
// filter nodes without children | |||||
(sourceNode.nodes && !sourceNode.nodes.length) || | |||||
// filter empty javascript and css nodes | |||||
(!sourceNode.nodes && !sourceNode.text)) { | |||||
return result => result | |||||
} | |||||
return curry(transformer)(sourceNode, source, meta) | |||||
} | |||||
// This function can be used to register new preprocessors | |||||
// a preprocessor can target either only the css or javascript nodes | |||||
// or the complete tag source file ('template') | |||||
export const registerPreprocessor = registerPreproc | |||||
// This function can allow you to register postprocessors that will parse the output code | |||||
// here we can run prettifiers, eslint fixes... | |||||
export const registerPostprocessor = registerPostproc |
@ -0,0 +1,53 @@ | |||||
import composeSourcemaps from './utils/compose-sourcemaps' | |||||
import { createOutput } from './transformer' | |||||
import panic from './utils/panic' | |||||
export const postprocessors = new Set() | |||||
/** | |||||
* Register a postprocessor that will be used after the parsing and compilation of the riot tags | |||||
* @param { Function } postprocessor - transformer that will receive the output code ans sourcemap | |||||
* @returns { Set } the postprocessors collection | |||||
*/ | |||||
export function register(postprocessor) { | |||||
if (postprocessors.has(postprocessor)) { | |||||
panic(`This postprocessor "${postprocessor.name || postprocessor.toString()}" was already registered`) | |||||
} | |||||
postprocessors.add(postprocessor) | |||||
return postprocessors | |||||
} | |||||
/** | |||||
* Unregister a postprocessor | |||||
* @param { Function } postprocessor - possibly a postprocessor previously registered | |||||
* @returns { Set } the postprocessors collection | |||||
*/ | |||||
export function unregister(postprocessor) { | |||||
if (!postprocessors.has(postprocessor)) { | |||||
panic(`This postprocessor "${postprocessor.name || postprocessor.toString()}" was never registered`) | |||||
} | |||||
postprocessors.delete(postprocessor) | |||||
return postprocessors | |||||
} | |||||
/** | |||||
* Exec all the postprocessors in sequence combining the sourcemaps generated | |||||
* @param { Output } compilerOutput - output generated by the compiler | |||||
* @param { Object } meta - compiling meta information | |||||
* @returns { Output } object containing output code and source map | |||||
*/ | |||||
export function execute(compilerOutput, meta) { | |||||
return Array.from(postprocessors).reduce(function(acc, postprocessor) { | |||||
const { code, map } = acc | |||||
const output = postprocessor(code, meta) | |||||
return { | |||||
code: output.code, | |||||
map: composeSourcemaps(map, output.map) | |||||
} | |||||
}, createOutput(compilerOutput, meta)) | |||||
} |
@ -0,0 +1,72 @@ | |||||
import panic from './utils/panic' | |||||
import { transform } from './transformer' | |||||
/** | |||||
* Parsers that can be registered by users to preparse components fragments | |||||
* @type { Object } | |||||
*/ | |||||
export const preprocessors = Object.freeze({ | |||||
javascript: new Map(), | |||||
css: new Map(), | |||||
template: new Map().set('default', code => ({ code })) | |||||
}) | |||||
// throw a processor type error | |||||
function preprocessorTypeError(type) { | |||||
panic(`No preprocessor of type "${type}" was found, please make sure to use one of these: 'javascript', 'css' or 'template'`) | |||||
} | |||||
// throw an error if the preprocessor was not registered | |||||
function preprocessorNameNotFoundError(name) { | |||||
panic(`No preprocessor named "${name}" was found, are you sure you have registered it?'`) | |||||
} | |||||
/** | |||||
* Register a custom preprocessor | |||||
* @param { string } type - preprocessor type either 'js', 'css' or 'template' | |||||
* @param { string } name - unique preprocessor id | |||||
* @param { Function } preprocessor - preprocessor function | |||||
* @returns { Map } - the preprocessors map | |||||
*/ | |||||
export function register(type, name, preprocessor) { | |||||
if (!type) panic('Please define the type of preprocessor you want to register \'javascript\', \'css\' or \'template\'') | |||||
if (!name) panic('Please define a name for your preprocessor') | |||||
if (!preprocessor) panic('Please provide a preprocessor function') | |||||
if (!preprocessors[type]) preprocessorTypeError(type) | |||||
if (preprocessors[type].has(name)) panic(`The preprocessor ${name} was already registered before`) | |||||
preprocessors[type].set(name, preprocessor) | |||||
return preprocessors | |||||
} | |||||
/** | |||||
* Register a custom preprocessor | |||||
* @param { string } type - preprocessor type either 'js', 'css' or 'template' | |||||
* @param { string } name - unique preprocessor id | |||||
* @returns { Map } - the preprocessors map | |||||
*/ | |||||
export function unregister(type, name) { | |||||
if (!type) panic('Please define the type of preprocessor you want to unregister \'javascript\', \'css\' or \'template\'') | |||||
if (!name) panic('Please define the name of the preprocessor you want to unregister') | |||||
if (!preprocessors[type]) preprocessorTypeError(type) | |||||
if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name) | |||||
preprocessors[type].delete(name) | |||||
return preprocessors | |||||
} | |||||
/** | |||||
* Exec the compilation of a preprocessor | |||||
* @param { string } type - preprocessor type either 'js', 'css' or 'template' | |||||
* @param { string } name - unique preprocessor id | |||||
* @param { Object } meta - preprocessor meta information | |||||
* @param { string } source - source code | |||||
* @returns { Output } object containing a sourcemap and a code string | |||||
*/ | |||||
export function execute(type, name, meta, source) { | |||||
if (!preprocessors[type]) preprocessorTypeError(type) | |||||
if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name) | |||||
return transform(preprocessors[type].get(name), meta, source) | |||||
} |
@ -0,0 +1,45 @@ | |||||
import createSourcemap from './utils/create-sourcemap' | |||||
export const Output = Object.freeze({ | |||||
code: '', | |||||
ast: [], | |||||
meta: {}, | |||||
map: null | |||||
}) | |||||
/** | |||||
* Create the right output data result of a parsing | |||||
* @param { Object } data - output data | |||||
* @param { string } data.code - code generated | |||||
* @param { AST } data.ast - ast representing the code | |||||
* @param { SourceMapGenerator } data.map - source map generated along with the code | |||||
* @param { Object } meta - compilation meta infomration | |||||
* @returns { Output } output container object | |||||
*/ | |||||
export function createOutput(data, meta) { | |||||
const output = { | |||||
...Output, | |||||
...data, | |||||
meta | |||||
} | |||||
if (!output.map && meta && meta.options && meta.options.file) | |||||
return { | |||||
...output, | |||||
map: createSourcemap({ file: meta.options.file }) | |||||
} | |||||
return output | |||||
} | |||||
/** | |||||
* Transform the source code received via a compiler function | |||||
* @param { Function } compiler - function needed to generate the output code | |||||
* @param { Object } meta - compilation meta information | |||||
* @param { string } source - source code | |||||
* @returns { Output } output - the result of the compiler | |||||
*/ | |||||
export function transform(compiler, meta, source) { | |||||
const result = (compiler ? compiler(source, meta) : { code: source }) | |||||
return createOutput(result, meta) | |||||
} |
@ -0,0 +1,13 @@ | |||||
import getLineAndColumnByPosition from './get-line-and-column-by-position' | |||||
/** | |||||
* Add the offset to the code that must be parsed in order to generate properly the sourcemaps | |||||
* @param {string} input - input string | |||||
* @param {string} source - original source code | |||||
* @param {RiotParser.Node} node - node that we are going to transform | |||||
* @return {string} the input string with the offset properly set | |||||
*/ | |||||
export default function addLineOffset(input, source, node) { | |||||
const {column, line} = getLineAndColumnByPosition(source, node.start) | |||||
return `${'\n'.repeat(line - 1)}${' '.repeat(column + 1)}${input}` | |||||
} |
@ -0,0 +1,19 @@ | |||||
import globalScope from 'globals' | |||||
import {namedTypes} from './build-types' | |||||
const browserAPIs = Object.keys(globalScope.browser) | |||||
const builtinAPIs = Object.keys(globalScope.builtin) | |||||
export const isIdentifier = namedTypes.Identifier.check.bind(namedTypes.Identifier) | |||||
export const isLiteral = namedTypes.Literal.check.bind(namedTypes.Literal) | |||||
export const isExpressionStatement = namedTypes.ExpressionStatement.check.bind(namedTypes.ExpressionStatement) | |||||
export const isObjectExpression = namedTypes.ObjectExpression.check.bind(namedTypes.ObjectExpression) | |||||
export const isThisExpression = namedTypes.ThisExpression.check.bind(namedTypes.ThisExpression) | |||||
export const isNewExpression = namedTypes.NewExpression.check.bind(namedTypes.NewExpression) | |||||
export const isSequenceExpression = namedTypes.SequenceExpression.check.bind(namedTypes.SequenceExpression) | |||||
export const isBinaryExpression = namedTypes.BinaryExpression.check.bind(namedTypes.BinaryExpression) | |||||
export const isExportDefaultStatement = namedTypes.ExportDefaultDeclaration.check.bind(namedTypes.ExportDefaultDeclaration) | |||||
export const isBrowserAPI = ({name}) => browserAPIs.includes(name) | |||||
export const isBuiltinAPI = ({name}) => builtinAPIs.includes(name) | |||||
export const isRaw = (node) => node && node.raw // eslint-disable-line |
@ -0,0 +1,5 @@ | |||||
import {types as astTypes} from 'recast' | |||||
export const types = astTypes | |||||
export const builders = astTypes.builders | |||||
export const namedTypes = astTypes.namedTypes |
@ -0,0 +1,8 @@ | |||||
/** | |||||
* Simple clone deep function, do not use it for classes or recursive objects! | |||||
* @param {*} source - possibily an object to clone | |||||
* @returns {*} the object we wanted to clone | |||||
*/ | |||||
export default function cloneDeep(source) { | |||||
return JSON.parse(JSON.stringify(source)) | |||||
} |
@ -0,0 +1,22 @@ | |||||
import asJSON from './sourcemap-as-json' | |||||
import {composeSourceMaps} from 'recast/lib/util' | |||||
import isNode from './is-node' | |||||
/** | |||||
* Compose two sourcemaps | |||||
* @param { SourceMapGenerator } formerMap - original sourcemap | |||||
* @param { SourceMapGenerator } latterMap - target sourcemap | |||||
* @returns { Object } sourcemap json | |||||
*/ | |||||
export default function composeSourcemaps(formerMap, latterMap) { | |||||
if ( | |||||
isNode() && | |||||
formerMap && latterMap && latterMap.mappings | |||||
) { | |||||
return composeSourceMaps(asJSON(formerMap), asJSON(latterMap)) | |||||
} else if (isNode() && formerMap) { | |||||
return asJSON(formerMap) | |||||
} | |||||
return {} | |||||
} |
@ -0,0 +1,10 @@ | |||||
import { SourceMapGenerator } from 'source-map' | |||||
/** | |||||
* Create a new sourcemap generator | |||||
* @param { Object } options - sourcemap options | |||||
* @returns { SourceMapGenerator } SourceMapGenerator instance | |||||
*/ | |||||
export default function createSourcemap(options) { | |||||
return new SourceMapGenerator(options) | |||||
} |
@ -0,0 +1,9 @@ | |||||
import {builders} from './build-types' | |||||
export function nullNode() { | |||||
return builders.literal(null) | |||||
} | |||||
export function simplePropertyNode(key, value) { | |||||
return builders.property('init', builders.literal(key), value, false) | |||||
} |
@ -0,0 +1,22 @@ | |||||
import {Parser} from 'acorn' | |||||
import {parse} from 'recast' | |||||
/** | |||||
* Parse a js source to generate the AST | |||||
* @param {string} source - javascript source | |||||
* @param {Object} options - parser options | |||||
* @returns {AST} AST tree | |||||
*/ | |||||
export default function generateAST(source, options) { | |||||
return parse(source, { | |||||
parser: { | |||||
parse(source, opts) { | |||||
return Parser.parse(source, { | |||||
...opts, | |||||
ecmaVersion: 2020 | |||||
}) | |||||
} | |||||
}, | |||||
...options | |||||
}) | |||||
} |
@ -0,0 +1,15 @@ | |||||
import {print} from 'recast' | |||||
/** | |||||
* Generate the javascript from an ast source | |||||
* @param {AST} ast - ast object | |||||
* @param {Object} options - printer options | |||||
* @returns {Object} code + map | |||||
*/ | |||||
export default function generateJavascript(ast, options) { | |||||
return print(ast, { | |||||
...options, | |||||
tabWidth: 2, | |||||
quote: 'single' | |||||
}) | |||||
} |
@ -0,0 +1,16 @@ | |||||
import splitStringByEOL from './split-string-by-EOL' | |||||
/** | |||||
* Get the line and the column of a source text based on its position in the string | |||||
* @param { string } string - target string | |||||
* @param { number } position - target position | |||||
* @returns { Object } object containing the source text line and column | |||||
*/ | |||||
export default function getLineAndColumnByPosition(string, position) { | |||||
const lines = splitStringByEOL(string.slice(0, position)) | |||||
return { | |||||
line: lines.length, | |||||
column: lines[lines.length - 1].length | |||||
} | |||||
} |
@ -0,0 +1,24 @@ | |||||
const ATTRIBUTE_TYPE_NAME = 'type' | |||||
/** | |||||
* Get the type attribute from a node generated by the riot parser | |||||
* @param { Object} sourceNode - riot parser node | |||||
* @returns { string|null } a valid type to identify the preprocessor to use or nothing | |||||
*/ | |||||
export default function getPreprocessorTypeByAttribute(sourceNode) { | |||||
const typeAttribute = sourceNode.attributes ? | |||||
sourceNode.attributes.find(attribute => attribute.name === ATTRIBUTE_TYPE_NAME) : | |||||
null | |||||
return typeAttribute ? normalize(typeAttribute.value) : null | |||||
} | |||||
/** | |||||
* Remove the noise in case a user has defined the preprocessor type='text/scss' | |||||
* @param { string } value - input string | |||||
* @returns { string } normalized string | |||||
*/ | |||||
function normalize(value) { | |||||
return value.replace('text/', '') | |||||
} |
@ -0,0 +1,8 @@ | |||||
/** | |||||
* True if the sourcemap has no mappings, it is empty | |||||
* @param {Object} map - sourcemap json | |||||
* @returns {boolean} true if empty | |||||
*/ | |||||
export default function isEmptySourcemap(map) { | |||||
return !map || !map.mappings || !map.mappings.length | |||||
} |
@ -0,0 +1,7 @@ | |||||
/** | |||||
* Detect node js environements | |||||
* @returns { boolean } true if the runtime is node | |||||
*/ | |||||
export default function isNode() { | |||||
return typeof process !== 'undefined' | |||||
} |
@ -0,0 +1,8 @@ | |||||
/** | |||||
* Throw an error with a descriptive message | |||||
* @param { string } message - error message | |||||
* @returns { undefined } hoppla.. at this point the program should stop working | |||||
*/ | |||||
export default function panic(message) { | |||||
throw new Error(message) | |||||
} |
@ -0,0 +1,18 @@ | |||||
import {execute as runPreprocessor} from '../preprocessors' | |||||
/** | |||||
* Preprocess a riot parser node | |||||
* @param { string } preprocessorType - either css, js | |||||
* @param { string } preprocessorName - preprocessor id | |||||
* @param { Object } meta - compilation meta information | |||||
* @param { RiotParser.nodeTypes } node - css node detected by the parser | |||||
* @returns { Output } code and sourcemap generated by the preprocessor | |||||
*/ | |||||
export default function preprocess(preprocessorType, preprocessorName, meta, node) { | |||||
const code = node.text | |||||
return (preprocessorName ? | |||||
runPreprocessor(preprocessorType, preprocessorName, meta, code) : | |||||
{ code } | |||||
) | |||||
} |
@ -0,0 +1,10 @@ | |||||
/** | |||||
* Return a source map as JSON, it it has not the toJSON method it means it can | |||||
* be used right the way | |||||
* @param { SourceMapGenerator|Object } map - a sourcemap generator or simply an json object | |||||
* @returns { Object } the source map as JSON | |||||
*/ | |||||
export default function sourcemapAsJSON(map) { | |||||
if (map && map.toJSON) return map.toJSON() | |||||
return map | |||||
} |
@ -0,0 +1,10 @@ | |||||
const LINES_RE = /\r\n?|\n/g | |||||
/** | |||||
* Split a string into a rows array generated from its EOL matches | |||||
* @param { string } string [description] | |||||
* @returns { Array } array containing all the string rows | |||||
*/ | |||||
export default function splitStringByEOL(string) { | |||||
return string.split(LINES_RE) | |||||
} |
@ -0,0 +1,9 @@ | |||||
/** | |||||
* Unescape the user escaped chars | |||||
* @param {string} string - input string | |||||
* @param {string} char - probably a '{' or anything the user want's to escape | |||||
* @returns {string} cleaned up string | |||||
*/ | |||||
export default function unescapeChar(string, char) { | |||||
return string.replace(RegExp(`\\\\${char}`, 'gm'), char) | |||||
} |
@ -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. |
@ -0,0 +1,546 @@ | |||||
# dom-bindings | |||||
[![Build Status][travis-image]][travis-url] | |||||
[![Code Quality][codeclimate-image]][codeclimate-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] | |||||
## Usage | |||||
```js | |||||
import { template, expressionTypes } from '@riotjs/dom-bindings' | |||||
// Create the app template | |||||
const tmpl = template('<p><!----></p>', [{ | |||||
selector: 'p', | |||||
expressions: [ | |||||
{ | |||||
type: expressionTypes.TEXT, | |||||
childNodeIndex: 0, | |||||
evaluate: scope => scope.greeting, | |||||
}, | |||||
], | |||||
}]) | |||||
// Mount the template to any DOM node | |||||
const target = document.getElementById('app') | |||||
const app = tmpl.mount(target, { | |||||
greeting: 'Hello World' | |||||
}) | |||||
``` | |||||
[travis-image]:https://img.shields.io/travis/riot/dom-bindings.svg?style=flat-square | |||||
[travis-url]:https://travis-ci.org/riot/dom-bindings | |||||
[license-image]:http://img.shields.io/badge/license-MIT-000000.svg?style=flat-square | |||||
[license-url]:LICENSE | |||||
[npm-version-image]:http://img.shields.io/npm/v/@riotjs/dom-bindings.svg?style=flat-square | |||||
[npm-downloads-image]:http://img.shields.io/npm/dm/@riotjs/dom-bindings.svg?style=flat-square | |||||
[npm-url]:https://npmjs.org/package/@riotjs/dom-bindings | |||||
[coverage-image]:https://img.shields.io/coveralls/riot/dom-bindings/master.svg?style=flat-square | |||||
[coverage-url]:https://coveralls.io/r/riot/dom-bindings/?branch=master | |||||
[codeclimate-image]:https://api.codeclimate.com/v1/badges/d0b7c555a1673354d66f/maintainability | |||||
[codeclimate-url]:https://codeclimate.com/github/riot/dom-bindings/maintainability | |||||
## API | |||||
### template(String, Array) | |||||
The template method is the most important of this package. | |||||
It will create a `TemplateChunk` that could be mounted, updated and unmounted to any DOM node. | |||||
<details> | |||||
<summary>Details</summary> | |||||
A template will always need a string as first argument and a list of `Bindings` to work properly. | |||||
Consider the following example: | |||||
```js | |||||
const tmpl = template('<p><!----></p>', [{ | |||||
selector: 'p', | |||||
expressions: [ | |||||
{ | |||||
type: expressionTypes.TEXT, | |||||
childNodeIndex: 0, | |||||
evaluate: scope => scope.greeting | |||||
} | |||||
], | |||||
}]) | |||||
``` | |||||
The template object above will bind a [simple binding](#simple-binding) to the `<p>` tag. | |||||
</details> | |||||
### bindingTypes | |||||
Object containing all the type of bindings supported | |||||
### expressionTypes | |||||
Object containing all the expressions types supported | |||||
## Bindings | |||||
A binding is simply an object that will be used internally to map the data structure provided to a DOM tree. | |||||
<details> | |||||
<summary>Details</summary> | |||||
To create a binding object you might use the following properties: | |||||
- `expressions` | |||||
- type: `Array<Expression>` | |||||
- required: `true` | |||||
- description: array containing instructions to execute DOM manipulation on the node queried | |||||
- `type` | |||||
- type: `Number` | |||||
- default:`bindingTypes.SIMPLE` | |||||
- optional: `true` | |||||
- description: id of the binding to use on the node queried. This id must be one of the keys available in the `bindingTypes` object | |||||
- `selector` | |||||
- type: `String` | |||||
- default: binding root **HTMLElement** | |||||
- optional: `true` | |||||
- description: property to query the node element that needs to updated | |||||
The bindings supported are only of 4 different types: | |||||
- [`simple`](#simple-binding) to bind simply the expressions to a DOM structure | |||||
- [`each`](#each-binding) to render DOM lists | |||||
- [`if`](#if-binding) to handle conditional DOM structures | |||||
- [`tag`](#tag-binding) to mount a coustom tag template to any DOM node | |||||
Combining the bindings above we can map any javascript object to a DOM template. | |||||
</details> | |||||
### Simple Binding | |||||
These kind of bindings will be only used to connect the expressions to DOM nodes in order to manipulate them. | |||||
<details> | |||||
<summary>Details</summary> | |||||
**Simple bindings will never modify the DOM tree structure, they will only target a single node.**<br/> | |||||
A simple binding must always contain at least one of the following expression: | |||||
- `attribute` to update the node attributes | |||||
- `event` to set the event handling | |||||
- `text` to update the node content | |||||
- `value` to update the node value | |||||
For example, let's consider the following binding: | |||||
```js | |||||
const pGreetingBinding = { | |||||
selector: 'p', | |||||
expressions: [{ | |||||
type: expressionTypes.Text, | |||||
childNodeIndex: 0, | |||||
evaluate: scope => scope.greeting, | |||||
}] | |||||
} | |||||
template('<article><p><!----></p></article>', [pGreeting]) | |||||
``` | |||||
In this case we have created a binding to update only the content of a `p` tag.<br/> | |||||
*Notice that the `p` tag has an empty comment that will be replaced with the value of the binding expression whenever the template will be mounted* | |||||
</details> | |||||
#### Simple Binding Expressions | |||||
The simple binding supports DOM manipulations only via expressions. | |||||
<details> | |||||
<summary>Details</summary> | |||||
An expression object must have always at least the following properties: | |||||
- `evaluate` | |||||
- type: `Function` | |||||
- description: function that will receive the current template scope and will return the current expression value | |||||
- `type` | |||||
- type: `Number` | |||||
- description: id to find the expression we need to apply to the node. This id must be one of the keys available in the `expressionTypes` object | |||||
</details> | |||||
##### Attribute Expression | |||||
The attribute expression allows to update all the DOM node attributes. | |||||
<details> | |||||
<summary>Details</summary> | |||||
This expression might contain the optional `name` key to update a single attribute for example: | |||||
```js | |||||
// update only the class attribute | |||||
{ type: expressionTypes.ATTRIBUTE, name: 'class', evaluate(scope) { return scope.attr }} | |||||
``` | |||||
If the `name` key will not be defined and the return of the `evaluate` function will be an object, this expression will set all the pairs `key, value` as DOM attributes. <br/> | |||||
Given the current scope `{ attr: { class: 'hello', 'name': 'world' }}`, the following expression will allow to set all the object attributes: | |||||
```js | |||||
{ type: expressionTypes.ATTRIBUTE, evaluate(scope) { return scope.attr }} | |||||
``` | |||||
If the return value of the evaluate function will be a `Boolean` the attribute will be considered a boolean attribute like `checked` or `selected`... | |||||
</details> | |||||
##### Event Expression | |||||
The event expression is really simple, It must contain the `name` attribute and it will set the callback as `dom[name] = callback`. | |||||
<details> | |||||
<summary>Details</summary> | |||||
For example: | |||||
```js | |||||
// add an event listener | |||||
{ type: expressionTypes.EVENT, name: 'onclick', evaluate(scope) { return function() { console.log('Hello There') } }} | |||||
``` | |||||
To remove an event listener you should only `return null` via evaluate function: | |||||
```js | |||||
// remove an event listener | |||||
{ type: expressionTypes.EVENT, name: 'onclick', evaluate(scope) { return null } }} | |||||
``` | |||||
</details> | |||||
##### Text Expression | |||||
The text expression must contain the `childNodeIndex` that will be used to identify which childNode from the `element.childNodes` collection will need to update its text content. | |||||
<details> | |||||
<summary>Details</summary> | |||||
Given for example the following template: | |||||
```html | |||||
<p><b>Your name is:</b><i>user_icon</i><!----></p> | |||||
``` | |||||
we could use the following text expression to replace the CommentNode with a TextNode | |||||
```js | |||||
{ type: expressionTypes.TEXT, childNodeIndex: 2, evaluate(scope) { return 'Gianluca' } }} | |||||
``` | |||||
</details> | |||||
##### Value Expression | |||||
The value expression will just set the `element.value` with the value received from the evaluate function. | |||||
<details> | |||||
<summary>Details</summary> | |||||
It should be used only for form elements and it might look like the example below: | |||||
```js | |||||
{ type: expressionTypes.VALUE, evaluate(scope) { return scope.val }} | |||||
``` | |||||
</details> | |||||
### Each Binding | |||||
The `each` binding is used to create multiple DOM nodes of the same type. This binding is typically used in to render javascript collections. | |||||
<details> | |||||
<summary>Details</summary> | |||||
**`each` bindings will need a template that will be cloned, mounted and updated for all the instances of the collection.**<br/> | |||||
An each binding should contain the following properties: | |||||
- `itemName` | |||||
- type: `String` | |||||
- required: `true` | |||||
- description: name to identify the item object of the current iteration | |||||
- `indexName` | |||||
- type: `Number` | |||||
- optional: `true` | |||||
- description: name to identify the current item index | |||||
- `evaluate` | |||||
- type: `Function` | |||||
- required: `true` | |||||
- description: function that will return the collection to iterate | |||||
- `template` | |||||
- type: `TemplateChunk` | |||||
- required: `true` | |||||
- description: a dom-bindings template that will be used as skeleton for the DOM elements created | |||||
- `condition` | |||||
- type: `Function` | |||||
- optional: `true` | |||||
- description: function that can be used to filter the items from the collection | |||||
The each bindings have the highest [hierarchical priority](#bindings-hierarchy) compared to the other riot bindings. | |||||
The following binding will loop through the `scope.items` collection creating several `p` tags having as TextNode child value dependent loop item received | |||||
```js | |||||
const eachBinding = { | |||||
type: bindingTypes.EACH, | |||||
itemName: 'val', | |||||
indexName: 'index' | |||||
evaluate: scope => scope.items, | |||||
template: template('<!---->', [{ | |||||
expressions: [ | |||||
{ | |||||
type: expressionTypes.TEXT, | |||||
childNodeIndex: 0, | |||||
evaluate: scope => `${scope.val} - ${scope.index}` | |||||
} | |||||
] | |||||
} | |||||
} | |||||
template('<p></p>', [eachBinding]) | |||||
``` | |||||
</details> | |||||
### If Binding | |||||
The `if` bindings are needed to handle conditionally entire parts of your components templates | |||||
<details> | |||||
<summary>Details</summary> | |||||
**`if` bindings will need a template that will be mounted and unmounted depending on the return value of the evaluate function.**<br/> | |||||
An if binding should contain the following properties: | |||||
- `evaluate` | |||||
- type: `Function` | |||||
- required: `true` | |||||
- description: if this function will return truthy values the template will be mounted otherwise unmounted | |||||
- `template` | |||||
- type: `TemplateChunk` | |||||
- required: `true` | |||||
- description: a dom-bindings template that will be used as skeleton for the DOM element created | |||||
The following binding will render the `b` tag only if the `scope.isVisible` property will be truthy. Otherwise the `b` tag will be removed from the template | |||||
```js | |||||
const ifBinding = { | |||||
type: bindingTypes.IF, | |||||
evaluate: scope => scope.isVisible, | |||||
selector: 'b' | |||||
template: template('<!---->', [{ | |||||
expressions: [ | |||||
{ | |||||
type: expressionTypes.TEXT, | |||||
childNodeIndex: 0, | |||||
evaluate: scope => scope.name | |||||
} | |||||
] | |||||
}]) | |||||
} | |||||
template('<p>Hello there <b></b></p>', [ifBinding]) | |||||
``` | |||||
</details> | |||||
### Tag Binding | |||||
The `tag` bindings are needed to mount custom components implementations | |||||
<details> | |||||
<summary>Details</summary> | |||||
`tag` bindings will enhance any child node with a custom component factory function. These bindings are likely riot components that must be mounted as children in a parent component template | |||||
A tag binding might contain the following properties: | |||||
- `getComponent` | |||||
- type: `Function` | |||||
- required: `true` | |||||
- description: the factory function responsible for the tag creation | |||||
- `evaluate` | |||||
- type: `Function` | |||||
- required: `true` | |||||
- description: it will receive the current scope and it must return the component id that will be passed as first argument to the `getComponent` function | |||||
- `slots` | |||||
- type: `Array<Slot>` | |||||
- optional: `true` | |||||
- description: array containing the slots that must be mounted into the child tag | |||||
- `attributes` | |||||
- type: `Array<AttributeExpression>` | |||||
- optional: `true` | |||||
- description: array containing the attribute values that should be passed to the child tag | |||||
The following tag binding will upgrade the `time` tag using the `human-readable-time` template. | |||||
This is how the `human-readable-time` template might look like | |||||
```js | |||||
import moment from 'moment' | |||||
export default function HumanReadableTime({ attributes }) { | |||||
const dateTimeAttr = attributes.find(({ name }) => name === 'datetime') | |||||
return template('<!---->', [{ | |||||
expressions: [{ | |||||
type: expressionTypes.TEXT, | |||||
childNodeIndex: 0, | |||||
evaluate(scope) { | |||||
const dateTimeValue = dateTimeAttr.evaluate(scope) | |||||
return moment(new Date(dateTimeValue)).fromNow() | |||||
} | |||||
}, ...attributes.map(attr => { | |||||
return { | |||||
...attr, | |||||
type: expressionTypes.ATTRIBUTE | |||||
} | |||||
})] | |||||
}]) | |||||
} | |||||
``` | |||||
Here it's how the previous tag might be used in a `tag` binding | |||||
```js | |||||
import HumanReadableTime from './human-readable-time' | |||||
const tagBinding = { | |||||
type: bindingTypes.TAG, | |||||
evaluate: () => 'human-readable-time', | |||||
getComponent: () => HumanReadableTime, | |||||
selector: 'time', | |||||
attributes: [{ | |||||
evaluate: scope => scope.time, | |||||
name: 'datetime' | |||||
}] | |||||
} | |||||
template('<p>Your last commit was: <time></time></p>', [tagBinding]).mount(app, { | |||||
time: '2017-02-14' | |||||
}) | |||||
``` | |||||
The `tag` bindings have always a lower priority compared to the `if` and `each` bindings | |||||
</details> | |||||
#### Slot Binding | |||||
The slot binding will be used to manage nested slotted templates that will be update using parent scope | |||||
<details> | |||||
<summary>Details</summary> | |||||
An expression object must have always at least the following properties: | |||||
- `evaluate` | |||||
- type: `Function` | |||||
- description: function that will receive the current template scope and will return the current expression value | |||||
- `type` | |||||
- type: `Number` | |||||
- description: id to find the expression we need to apply to the node. This id must be one of the keys available in the `expressionTypes` object | |||||
- `name` | |||||
- type: `String` | |||||
- description: the name to identify the binding html we need to mount in this node | |||||
```js | |||||
// slots array that will be mounted receiving the scope of the parent template | |||||
const slots = [{ | |||||
id: 'foo', | |||||
bindings: [{ | |||||
selector: '[expr1]', | |||||
expressions: [{ | |||||
type: expressionTypes.TEXT, | |||||
childNodeIndex: 0, | |||||
evaluate: scope => scope.text | |||||
}] | |||||
}], | |||||
html: '<p expr1><!----></p>' | |||||
}] | |||||
const el = template('<article><slot expr0/></article>', [{ | |||||
type: bindingTypes.SLOT, | |||||
selector: '[expr0]', | |||||
name: 'foo' | |||||
}]).mount(app, { | |||||
slots | |||||
}, { text: 'hello' }) | |||||
``` | |||||
</details> | |||||
## Bindings Hierarchy | |||||
If the same DOM node has multiple bindings bound to it, they should be created following the order below: | |||||
1. Each Binding | |||||
2. If Binding | |||||
3. Tag Binding | |||||
<details> | |||||
<summary>Details</summary> | |||||
Let's see some cases where we might combine multiple bindings on the same DOM node and how to handle them properly. | |||||
### Each and If Bindings | |||||
Let's consider for example a DOM node that sould handle in parallel the Each and If bindings. | |||||
In that case we could skip the `If Binding` and just use the `condition` function provided by the [`Each Binding`](#each-binding) | |||||
Each bindings will handle conditional rendering internally without the need of extra logic. | |||||
### Each and Tag Bindings | |||||
A custom tag having an Each Binding bound to it should be handled giving the priority to the Eeach Binding. For example: | |||||
```js | |||||
const components = { | |||||
'my-tag': function({ slots, attributes }) { | |||||
return { | |||||
mount(el, scope) { | |||||
// do stuff on the mount | |||||
}, | |||||
unmount() { | |||||
// do stuff on the unmount | |||||
} | |||||
} | |||||
} | |||||
} | |||||
const el = template('<ul><li expr0></li></ul>', [{ | |||||
type: bindingTypes.EACH, | |||||
itemName: 'val', | |||||
selector: '[expr0]', | |||||
evaluate: scope => scope.items, | |||||
template: template(null, [{ | |||||
type: bindingTypes.TAG, | |||||
name: 'my-tag', | |||||
getComponent(name) { | |||||
// name here will be 'my-tag' | |||||
return components[name] | |||||
} | |||||
}]) | |||||
}]).mount(target, { items: [1, 2] }) | |||||
``` | |||||
The template for the Each Binding above will be created receiving `null` as first argument because we suppose that the custom tag template was already stored and registered somewhere else. | |||||
### If and Tag Bindings | |||||
Similar to the previous example, If Bindings have always the priority on the Tag Bindings. For example: | |||||
```js | |||||
const el = template('<ul><li expr0></li></ul>', [{ | |||||
type: bindingTypes.IF, | |||||
selector: '[expr0]', | |||||
evaluate: scope => scope.isVisible, | |||||
template: template(null, [{ | |||||
type: bindingTypes.TAG, | |||||
evaluate: () => 'my-tag', | |||||
getComponent(name) { | |||||
// name here will be 'my-tag' | |||||
return components[name] | |||||
} | |||||
}]) | |||||
}]).mount(target, { isVisible: true }) | |||||
``` | |||||
The template for the IF Binding will mount/unmount the Tag Binding on its own DOM node. | |||||
</details> | |||||
@ -0,0 +1,88 @@ | |||||
{ | |||||
"_from": "@riotjs/dom-bindings@^4.2.5", | |||||
"_id": "@riotjs/dom-bindings@4.3.0", | |||||
"_inBundle": false, | |||||
"_integrity": "sha512-SxcuWgW9wdUlJnMiyNgeC6n/Olo1SyiDmhSpR+Ss+iUKCnfjbl5lpq6AoBqpUiO++PMUIRZqsDsvkI+pilMSqw==", | |||||
"_location": "/@riotjs/dom-bindings", | |||||
"_phantomChildren": {}, | |||||
"_requested": { | |||||
"type": "range", | |||||
"registry": true, | |||||
"raw": "@riotjs/dom-bindings@^4.2.5", | |||||
"name": "@riotjs/dom-bindings", | |||||
"escapedName": "@riotjs%2fdom-bindings", | |||||
"scope": "@riotjs", | |||||
"rawSpec": "^4.2.5", | |||||
"saveSpec": null, | |||||
"fetchSpec": "^4.2.5" | |||||
}, | |||||
"_requiredBy": [ | |||||
"/@riotjs/compiler", | |||||
"/riot" | |||||
], | |||||
"_resolved": "https://registry.npmjs.org/@riotjs/dom-bindings/-/dom-bindings-4.3.0.tgz", | |||||
"_shasum": "38dd567814f4150f1fd6e3a94762d532d278b879", | |||||
"_spec": "@riotjs/dom-bindings@^4.2.5", | |||||
"_where": "/home/herrhase/Workspace/tentakelfabrik/tiny-components/tiny-one-page/node_modules/riot", | |||||
"author": { | |||||
"name": "Gianluca Guarini", | |||||
"email": "gianluca.guarini@gmail.com", | |||||
"url": "http://gianlucaguarini.com" | |||||
}, | |||||
"bugs": { | |||||
"url": "https://github.com/GianlucaGuarini/dom-bindings/issues" | |||||
}, | |||||
"bundleDependencies": false, | |||||
"dependencies": { | |||||
"domdiff": "^2.0.7" | |||||
}, | |||||
"deprecated": false, | |||||
"description": "Riot.js DOM bindings", | |||||
"devDependencies": { | |||||
"benchmark": "^2.1.4", | |||||
"chai": "^4.2.0", | |||||
"coveralls": "^3.0.6", | |||||
"eslint": "^6.3.0", | |||||
"eslint-config-riot": "^3.0.0", | |||||
"esm": "^3.2.25", | |||||
"jsdom": "15.1.1", | |||||
"jsdom-global": "3.0.2", | |||||
"mocha": "^6.2.0", | |||||
"nyc": "^14.1.1", | |||||
"rollup": "^1.20.3", | |||||
"rollup-plugin-alias": "^2.0.0", | |||||
"rollup-plugin-node-resolve": "^5.2.0", | |||||
"sinon": "^7.4.1", | |||||
"sinon-chai": "^3.3.0" | |||||
}, | |||||
"files": [ | |||||
"dist" | |||||
], | |||||
"homepage": "https://github.com/GianlucaGuarini/dom-bindings#readme", | |||||
"jsnext:main": "dist/esm.dom-bindings.js", | |||||
"keywords": [ | |||||
"riot", | |||||
"dom-bindings" | |||||
], | |||||
"license": "MIT", | |||||
"main": "dist/umd.dom-bindings.js", | |||||
"module": "dist/esm.dom-bindings.js", | |||||
"name": "@riotjs/dom-bindings", | |||||
"repository": { | |||||
"type": "git", | |||||
"url": "git+https://github.com/GianlucaGuarini/dom-bindings.git" | |||||
}, | |||||
"scripts": { | |||||
"bench": "node benchmarks", | |||||
"build": "rollup -c", | |||||
"cov": "nyc report --reporter=text-lcov | coveralls", | |||||
"cov-html": "nyc report --reporter=html", | |||||
"lint": "eslint src/ test/ rollup.config.js", | |||||
"postest": "npm run cov-html", | |||||
"prepublishOnly": "npm test", | |||||
"pretest": "npm run build", | |||||
"test": "npm run lint && nyc mocha -r esm test/index.js", | |||||
"test-debug": "mocha -r esm --inspect --inspect-brk test/index.js" | |||||
}, | |||||
"version": "4.3.0" | |||||
} |
@ -0,0 +1,89 @@ | |||||
# Changes for riot-parser | |||||
### v4.0.3 | |||||
- Fix https://github.com/riot/riot/issues/2723 for real this time | |||||
### v4.0.2 | |||||
- Fix parsing of nested svg nodes https://github.com/riot/riot/issues/2723 | |||||
### v4.0.1 | |||||
- Fix the creation of the `parts` array in nodes containing expressions | |||||
### v4.0.0 | |||||
- Stable release | |||||
- Add more tests for the new feautures listed below | |||||
### v4.0.0-rc.2 | |||||
- Fix: support spread attributes together with other attribute expressions on the same DOM node | |||||
### v4.0.0-rc.1 | |||||
- Fix https://github.com/riot/riot/issues/2679 | |||||
- Add support for `<a {href}>` expression attributes shortcuts | |||||
### v0.8.1 | |||||
- Add the `src` folder to the npm publishing files | |||||
### v0.8.0 | |||||
- Add support for the spread attributes `<a {...foo.bar}>` | |||||
- Fixed the `isCustom` boolean that will be added also to the root nodes | |||||
### v0.6.9 | |||||
- Remove the unecessary PUBLIC_JAVASCRIPT and PRIVATE_JAVASCRIPT nodes | |||||
### v0.5.0 | |||||
- Remove the the useless prefix option | |||||
- Improve the coverage | |||||
- Improve the quality of the source code | |||||
### v0.4.0 | |||||
- Add the [`dom-nodes`](https://github.com/riot/dom-nodes) dependecy to improve the output | |||||
- Add the `isCustom`, `isBoolean`, `isVoid`, `isSelfClosing` and `isRaw` boolean node attributes | |||||
### v0.3.0 | |||||
- Fix treeBuilder issues | |||||
- Improve coverage | |||||
- Improve code maintainability | |||||
### v0.2.0 | |||||
- Add `voidTags` to the exports | |||||
### v0.1.0 | |||||
- Enhance the javascript parsing: the javascript node will contain nested nodes containing the private and the public javascript methods | |||||
- Add the PUBLIC_JAVASCRIPT and PRIVATE_JAVASCRIPT nodes | |||||
- Change the `attr` to `attributes` and `expr` to `expressions` keys | |||||
### v0.0.6 | |||||
- Tree-builder support for 'if/else/elseif' tags (avoid unexpected closing tag errors). | |||||
- Fix to text nodes only escaping the fist block of whitespace. | |||||
### v0.0.5 | |||||
- Now, attribute names are lowercased in the builder, only for empty namespaces (i.e. not svg). | |||||
### v0.0.4 | |||||
- Included TEXTAREA as special tag that can contain only raw text and expressions. | |||||
- For SVG tags, now the `ns` property is the full URI http://www.w3.org/2000/svg. | |||||
- The `children` property of TAGs is renamed to `nodes`. | |||||
### v0.0.3 | |||||
- The default builder is integrated in this module and injected in the parser. | |||||
- Only two versions, node CommonJS (transpiled to ES5) and ES6 modules (untranspiled). | |||||
- The `nodeTypes` property of TagParser is removed, now is in a separated submodule. | |||||
- Exposing `skipES6TL` to skip ES6 Template Literals. | |||||
- Reduction of code size, `skipRegex` is imported from npm. | |||||
- Source files (ES6) are moved to the "lib/" directory. | |||||
- Remove dependency on `Object.assign`. | |||||
- Updated devDependencies. | |||||
### v0.0.2 (UNPUBLISHED) | |||||
- Added suport for SVG en the tests. | |||||
- Added test/builder/tree-builder2.js as sample. | |||||
- Support for self-closing script/style tags. | |||||
- The `replace` property of attributes and text is discarded and there's a new property `unescape` is an array containing the positions of the escape characters (relative to the whole buffer). | |||||
- Matching literal regexes is a bit faster now. | |||||
- Fixes incorrect regex that matches literal regexes. | |||||
### v0.0.1 | |||||
- First public release | |||||
# TODO | |||||
- Support for case sensitive properties in SVG elements. |
@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2018 Riot | |||||
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. |
@ -0,0 +1,140 @@ | |||||
# parser | |||||
[![Build Status][travis-image]][travis-url] | |||||
[![Code Quality][codeclimate-image]][codeclimate-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] | |||||
Minimal, loose html parser for Riot tags | |||||
### Install | |||||
```bash | |||||
npm i @riotjs/parser --save | |||||
``` | |||||
The package has two modules: | |||||
```js | |||||
// Use as: parser(options).parse(code, startPosition) | |||||
const parser = require('@riotjs/parser').default | |||||
// The enum NodeTypes (a plain JS object) that contains the values of the | |||||
// type property of the nodes emited by tagParser (and more). | |||||
const nodeTypes = require('@riotjs/parser').nodeTypes | |||||
``` | |||||
ES6 modules export: | |||||
```js | |||||
import parser, { nodeTypes } from '@riotjs/parser' | |||||
``` | |||||
This parser is a low-level tool that builds a simple array of objects with information about the given html fragment, readed secuencially. It is designed to parse one single tag and not entire html pages, the tag closing the root element ends the parsing. | |||||
There are 3 main node types: | |||||
* Tags - HTMLElements, including SCRIPT and STYLE elements. | |||||
* Comments - Ignored by default. | |||||
* Text - Text nodes. | |||||
Opening tags can contain attributes. Text and attribute values can contain expressions. | |||||
There's no support for untagged JavaScript block. | |||||
The value returned by the parser is an object like this: | |||||
```js | |||||
{ | |||||
data, // String of the given html fragment with no changes. | |||||
output // Array of objects with information about the parsed tags. | |||||
} | |||||
``` | |||||
The first element of `output` is the opening tag of the root element. | |||||
The parsing stops when the closing tag of the root is found, so the last node have the ending position. | |||||
### Commands | |||||
* Build: `npm run build` | |||||
* Test: `npm t` | |||||
* Samples: `npm run samples` | |||||
## Tag names | |||||
Both, html and Riot tag names must start with a 7 bit letter (`[a-zA-Z]`) followed by zero o more ISO-8859-1 characters, except those in `[\x00-\x2F\x7F-\xA0>/]`. | |||||
If the first letter is not found, it becomes simple text. | |||||
Any non-recognized character ends the tag name (`'/'` behaves like whitespace). | |||||
All the tag names are converted to lower case. | |||||
## Openning Tags | |||||
Start with a `'<'` followed by a [tag name](#tag-names) or the character `'!'` that signals the start of a [comment](#comments), `DOCTYPE` or `CDATA` declaration (last two are parsed as comments). | |||||
Against the html5 specs, tags ending with `'/>'` are preserved as self-closing tags (the builder must handle this). | |||||
## Closing tags | |||||
They are included in the output, except for void or self-closing tags, and its name include the first slash. | |||||
## Attributes | |||||
Accepts all characters as the tag names and more. | |||||
An equal sign (`'='`) separates the name of the value. If there's no name, this `'='` is the first character of the name (yes). The value can be empty. | |||||
One or more slashes (`'/'`) behaves like whitespace. In the name, the slash splits the name generating two attributes, even if the name was quoted. | |||||
The first `>` anywhere in the openning tag ends the attribute list, except if this is in a quoted value. | |||||
All attribute names are converted to lowercase and the unquoted values are trimmed. | |||||
## Comments | |||||
Must start with `'<!--'`. The next following `'-->'` or the end of file ends the comment. | |||||
Comments in short notation, starting with `'<!'` (without `'--'`), ends at the first `'>'`. | |||||
By default, comments are discarted. | |||||
## Expressions | |||||
Expressions may be contained in attribute values or text nodes. | |||||
The default delimiters are `'{'` and `'}'`. | |||||
There may be more tan one expression as part of one attribute value or text node, or only one replacing the entire value or node. | |||||
When used as the whole attribute value, there's no need to enclose the expression inside quotes, even if the expression contains whitespace. | |||||
Single and double quotes can be nested inside the expression. | |||||
To emit opening (left) brackets as literal text wherever an opening bracket is expected, the bracket must be prefixed with a backslash (the JS escape char `'\'`). | |||||
This character is preserved in the output, but the parser will add a `replace` property for the attribute or node containing the escaped bracket, whose value is the bracket itself. | |||||
## Options | |||||
* `comments` - Pass `true` to preserve the comments. | |||||
* `brackets` - Array of two string with the left/right brackets used to extract expressions. | |||||
[travis-image]:https://img.shields.io/travis/riot/parser.svg?style=flat-square | |||||
[travis-url]:https://travis-ci.org/riot/parser | |||||
[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/parser.svg?style=flat-square | |||||
[npm-downloads-image]:http://img.shields.io/npm/dm/@riotjs/parser.svg?style=flat-square | |||||
[npm-url]:https://npmjs.org/package/@riotjs/parser | |||||
[coverage-image]:https://img.shields.io/coveralls/riot/parser/master.svg?style=flat-square | |||||
[coverage-url]:https://coveralls.io/r/riot/parser/?branch=master | |||||
[codeclimate-image]:https://api.codeclimate.com/v1/badges/5db4f1c96a43e3736cf0/maintainability | |||||
[codeclimate-url]:https://codeclimate.com/github/riot/parser | |||||
@ -0,0 +1,89 @@ | |||||
{ | |||||
"_from": "@riotjs/parser@^4.0.3", | |||||
"_id": "@riotjs/parser@4.0.3", | |||||
"_inBundle": false, | |||||
"_integrity": "sha512-mP10ljXVJ2T0uD6FD9ZKRs1TZZiL6LcXRQJYUCbQ2tNePaKDfcsteuL3O+rk77zNwSgYfEPMSwk9gkcqCy7L7w==", | |||||
"_location": "/@riotjs/parser", | |||||
"_phantomChildren": {}, | |||||
"_requested": { | |||||
"type": "range", | |||||
"registry": true, | |||||
"raw": "@riotjs/parser@^4.0.3", | |||||
"name": "@riotjs/parser", | |||||
"escapedName": "@riotjs%2fparser", | |||||
"scope": "@riotjs", | |||||
"rawSpec": "^4.0.3", | |||||
"saveSpec": null, | |||||
"fetchSpec": "^4.0.3" | |||||
}, | |||||
"_requiredBy": [ | |||||
"/@riotjs/compiler" | |||||
], | |||||
"_resolved": "https://registry.npmjs.org/@riotjs/parser/-/parser-4.0.3.tgz", | |||||
"_shasum": "0d8bbd3067cfc3d3984c8921d9e4a257b977ac9e", | |||||
"_spec": "@riotjs/parser@^4.0.3", | |||||
"_where": "/home/herrhase/Workspace/tentakelfabrik/tiny-components/tiny-one-page/node_modules/@riotjs/compiler", | |||||
"author": { | |||||
"name": "aMarCruz", | |||||
"email": "amarcruz@yahoo.com", | |||||
"url": "https://github.com/aMarCruz" | |||||
}, | |||||
"bugs": { | |||||
"url": "https://github.com/riot/parser/issues" | |||||
}, | |||||
"bundleDependencies": false, | |||||
"dependencies": { | |||||
"curri": "^1.0.1", | |||||
"dom-nodes": "^1.0.0" | |||||
}, | |||||
"deprecated": false, | |||||
"description": "The parser for Riot tags", | |||||
"devDependencies": { | |||||
"chai": "^4.2.0", | |||||
"coveralls": "^3.0.4", | |||||
"eslint": "^6.0.1", | |||||
"eslint-config-riot": "^2.0.0", | |||||
"mocha": "^6.1.4", | |||||
"nyc": "^14.1.1", | |||||
"rollup": "^1.16.3", | |||||
"rollup-plugin-node-resolve": "^5.2.0" | |||||
}, | |||||
"engines": { | |||||
"node": ">=4.2", | |||||
"npm": ">=3.0" | |||||
}, | |||||
"files": [ | |||||
"*.js", | |||||
"dist", | |||||
"src" | |||||
], | |||||
"homepage": "https://github.com/riot/parser", | |||||
"jsnext:main": "./src/index.js", | |||||
"keywords": [ | |||||
"html", | |||||
"html5", | |||||
"tag", | |||||
"parser", | |||||
"javascript" | |||||
], | |||||
"license": "MIT", | |||||
"main": "./index.js", | |||||
"module": "./src/index.js", | |||||
"name": "@riotjs/parser", | |||||
"repository": { | |||||
"type": "git", | |||||
"url": "git+https://github.com/riot/parser.git" | |||||
}, | |||||
"scripts": { | |||||
"build": "rollup -c", | |||||
"cov": "nyc report --reporter=text-lcov | coveralls", | |||||
"cov-html": "nyc report --reporter=html", | |||||
"lint": "eslint src test", | |||||
"prepublish": "npm run build", | |||||
"pretest": "npm run build", | |||||
"samples": "node ./test/samples.js", | |||||
"test": "nyc mocha ./test/index", | |||||
"test-debug": "mocha --inspect-brk ./test" | |||||
}, | |||||
"version": "4.0.3" | |||||
} |
@ -0,0 +1,11 @@ | |||||
import resolve from 'rollup-plugin-node-resolve' | |||||
export default { | |||||
input: 'src/index.js', | |||||
output: { | |||||
name: 'parser', | |||||
format: 'cjs', | |||||
file: './index.js' | |||||
}, | |||||
plugins: [resolve()] | |||||
} |
@ -0,0 +1,16 @@ | |||||
export const JAVASCRIPT_OUTPUT_NAME = 'javascript' | |||||
export const CSS_OUTPUT_NAME = 'css' | |||||
export const TEMPLATE_OUTPUT_NAME = 'template' | |||||
// Tag names | |||||
export const JAVASCRIPT_TAG = 'script' | |||||
export const STYLE_TAG = 'style' | |||||
export const TEXTAREA_TAG = 'textarea' | |||||
// Boolean attributes | |||||
export const IS_RAW = 'isRaw' | |||||
export const IS_SELF_CLOSING = 'isSelfClosing' | |||||
export const IS_VOID = 'isVoid' | |||||
export const IS_BOOLEAN = 'isBoolean' | |||||
export const IS_CUSTOM = 'isCustom' | |||||
export const IS_SPREAD = 'isSpread' |
@ -0,0 +1,12 @@ | |||||
import * as types from './node-types' | |||||
import parser from './parser' | |||||
/** | |||||
* The nodeTypes definition | |||||
*/ | |||||
export const nodeTypes = types | |||||
/* | |||||
* Factory function to create instances of the parser | |||||
*/ | |||||
export default parser |
@ -0,0 +1,10 @@ | |||||
export const rootTagNotFound = 'Root tag not found.' | |||||
export const unclosedTemplateLiteral = 'Unclosed ES6 template literal.' | |||||
export const unexpectedEndOfFile = 'Unexpected end of file.' | |||||
export const unexpectedNamedTag = 'Unexpected tag <%1>' | |||||
export const unclosedComment = 'Unclosed comment.' | |||||
export const unclosedNamedBlock = 'Unclosed "%1" block.' | |||||
export const duplicatedNamedTag = 'Duplicate tag "<%1>".' | |||||
export const expectedAndInsteadSaw = 'Expected "</%1>" and instead saw "<%2>".' | |||||
export const unexpectedCharInExpression = 'Unexpected character %1.' | |||||
export const unclosedExpression = 'Unclosed expression.' |
@ -0,0 +1,14 @@ | |||||
/** | |||||
* Not all the types are handled in this module. | |||||
* | |||||
* @enum {number} | |||||
* @readonly | |||||
*/ | |||||
export const TAG = 1 /* TAG */ | |||||
export const ATTR = 2 /* ATTR */ | |||||
export const TEXT = 3 /* TEXT */ | |||||
export const CDATA = 4 /* CDATA */ | |||||
export const COMMENT = 8 /* COMMENT */ | |||||
export const DOCUMENT = 9 /* DOCUMENT */ | |||||
export const DOCTYPE = 10 /* DOCTYPE */ | |||||
export const DOCUMENT_FRAGMENT = 11 /* DOCUMENT_FRAGMENT */ |
@ -0,0 +1,112 @@ | |||||
import {ATTR, TAG} from './node-types' | |||||
import {rootTagNotFound, unexpectedEndOfFile} from './messages' | |||||
import attr from './parsers/attribute' | |||||
import curry from 'curri' | |||||
import flush from './utils/flush-parser-state' | |||||
import panic from './utils/panic' | |||||
import tag from './parsers/tag' | |||||
import text from './parsers/text' | |||||
import treeBuilder from './tree-builder' | |||||
/** | |||||
* Factory for the Parser class, exposing only the `parse` method. | |||||
* The export adds the Parser class as property. | |||||
* | |||||
* @param {Object} options - User Options | |||||
* @param {Function} customBuilder - Tree builder factory | |||||
* @returns {Function} Public Parser implementation. | |||||
*/ | |||||
export default function parser(options, customBuilder) { | |||||
const state = curry(createParserState)(options, customBuilder || treeBuilder) | |||||
return { | |||||
parse: (data) => parse(state(data)) | |||||
} | |||||
} | |||||
/** | |||||
* Create a new state object | |||||
* @param {Object} userOptions - parser options | |||||
* @param {Function} builder - Tree builder factory | |||||
* @param {string} data - data to parse | |||||
* @returns {ParserState} it represents the current parser state | |||||
*/ | |||||
function createParserState(userOptions, builder, data) { | |||||
const options = Object.assign({ | |||||
brackets: ['{', '}'] | |||||
}, userOptions) | |||||
return { | |||||
options, | |||||
regexCache: {}, | |||||
pos: 0, | |||||
count: -1, | |||||
root: null, | |||||
last: null, | |||||
scryle: null, | |||||
builder: builder(data, options), | |||||
data | |||||
} | |||||
} | |||||
/** | |||||
* It creates a raw output of pseudo-nodes with one of three different types, | |||||
* all of them having a start/end position: | |||||
* | |||||
* - TAG -- Opening or closing tags | |||||
* - TEXT -- Raw text | |||||
* - COMMENT -- Comments | |||||
* | |||||
* @param {ParserState} state - Current parser state | |||||
* @returns {ParserResult} Result, contains data and output properties. | |||||
*/ | |||||
function parse(state) { | |||||
const { data } = state | |||||
walk(state) | |||||
flush(state) | |||||
if (state.count) { | |||||
panic(data, state.count > 0 ? unexpectedEndOfFile : rootTagNotFound, state.pos) | |||||
} | |||||
return { | |||||
data, | |||||
output: state.builder.get() | |||||
} | |||||
} | |||||
/** | |||||
* Parser walking recursive function | |||||
* @param {ParserState} state - Current parser state | |||||
* @param {string} type - current parsing context | |||||
* @returns {undefined} void function | |||||
*/ | |||||
function walk(state, type) { | |||||
const { data } = state | |||||
// extend the state adding the tree builder instance and the initial data | |||||
const length = data.length | |||||
// The "count" property is set to 1 when the first tag is found. | |||||
// This becomes the root and precedent text or comments are discarded. | |||||
// So, at the end of the parsing count must be zero. | |||||
if (state.pos < length && state.count) { | |||||
walk(state, eat(state, type)) | |||||
} | |||||
} | |||||
/** | |||||
* Function to help iterating on the current parser state | |||||
* @param {ParserState} state - Current parser state | |||||
* @param {string} type - current parsing context | |||||
* @returns {string} parsing context | |||||
*/ | |||||
function eat(state, type) { | |||||
switch (type) { | |||||
case TAG: | |||||
return tag(state) | |||||
case ATTR: | |||||
return attr(state) | |||||
default: | |||||
return text(state) | |||||
} | |||||
} |
@ -0,0 +1,183 @@ | |||||
import {ATTR, TEXT} from '../node-types' | |||||
import {ATTR_START, SPREAD_OPERATOR} from '../regex' | |||||
import {IS_BOOLEAN, IS_SELF_CLOSING, IS_SPREAD} from '../constants' | |||||
import addToCollection from '../utils/add-to-collection' | |||||
import execFromPos from '../utils/exec-from-pos' | |||||
import expr from './expression' | |||||
import getChunk from '../utils/get-chunk' | |||||
import {isBoolAttribute} from 'dom-nodes' | |||||
import memoize from '../utils/memoize' | |||||
const expressionsContentRe = memoize(brackets => RegExp(`(${brackets[0]}[^${brackets[1]}]*?${brackets[1]})`, 'g')) | |||||
const isSpreadAttribute = name => SPREAD_OPERATOR.test(name) | |||||
const isAttributeExpression = (name, brackets) => name[0] === brackets[0] | |||||
const getAttributeEnd = (state, attr) => expr(state, attr, '[>/\\s]', attr.start) | |||||
/** | |||||
* The more complex parsing is for attributes as it can contain quoted or | |||||
* unquoted values or expressions. | |||||
* | |||||
* @param {ParserStore} state - Parser state | |||||
* @returns {number} New parser mode. | |||||
* @private | |||||
*/ | |||||
export default function attr(state) { | |||||
const { data, last, pos, root } = state | |||||
const tag = last // the last (current) tag in the output | |||||
const _CH = /\S/g // matches the first non-space char | |||||
const ch = execFromPos(_CH, pos, data) | |||||
switch (true) { | |||||
case !ch: | |||||
state.pos = data.length // reaching the end of the buffer with | |||||
// NodeTypes.ATTR will generate error | |||||
break | |||||
case ch[0] === '>': | |||||
// closing char found. If this is a self-closing tag with the name of the | |||||
// Root tag, we need decrement the counter as we are changing mode. | |||||
state.pos = tag.end = _CH.lastIndex | |||||
if (tag[IS_SELF_CLOSING]) { | |||||
state.scryle = null // allow selfClosing script/style tags | |||||
if (root && root.name === tag.name) { | |||||
state.count-- // "pop" root tag | |||||
} | |||||
} | |||||
return TEXT | |||||
case ch[0] === '/': | |||||
state.pos = _CH.lastIndex // maybe. delegate the validation | |||||
tag[IS_SELF_CLOSING] = true // the next loop | |||||
break | |||||
default: | |||||
delete tag[IS_SELF_CLOSING] // ensure unmark as selfclosing tag | |||||
setAttribute(state, ch.index, tag) | |||||
} | |||||
return ATTR | |||||
} | |||||
/** | |||||
* Parses an attribute and its expressions. | |||||
* | |||||
* @param {ParserStore} state - Parser state | |||||
* @param {number} pos - Starting position of the attribute | |||||
* @param {Object} tag - Current parent tag | |||||
* @returns {undefined} void function | |||||
* @private | |||||
*/ | |||||
function setAttribute(state, pos, tag) { | |||||
const { data } = state | |||||
const expressionContent = expressionsContentRe(state.options.brackets) | |||||
const re = ATTR_START // (\S[^>/=\s]*)(?:\s*=\s*([^>/])?)? g | |||||
const start = re.lastIndex = expressionContent.lastIndex = pos // first non-whitespace | |||||
const attrMatches = re.exec(data) | |||||
const isExpressionName = isAttributeExpression(attrMatches[1], state.options.brackets) | |||||
const match = isExpressionName ? [null, expressionContent.exec(data)[1], null] : attrMatches | |||||
if (match) { | |||||
const end = re.lastIndex | |||||
const attr = parseAttribute(state, match, start, end, isExpressionName) | |||||
//assert(q && q.type === Mode.TAG, 'no previous tag for the attr!') | |||||
// Pushes the attribute and shifts the `end` position of the tag (`last`). | |||||
state.pos = tag.end = attr.end | |||||
tag.attributes = addToCollection(tag.attributes, attr) | |||||
} | |||||
} | |||||
function parseNomalAttribute(state, attr, quote) { | |||||
const { data } = state | |||||
let { end } = attr | |||||
if (isBoolAttribute(attr.name)) { | |||||
attr[IS_BOOLEAN] = true | |||||
} | |||||
// parse the whole value (if any) and get any expressions on it | |||||
if (quote) { | |||||
// Usually, the value's first char (`quote`) is a quote and the lastIndex | |||||
// (`end`) is the start of the value. | |||||
let valueStart = end | |||||
// If it not, this is an unquoted value and we need adjust the start. | |||||
if (quote !== '"' && quote !== '\'') { | |||||
quote = '' // first char of value is not a quote | |||||
valueStart-- // adjust the starting position | |||||
} | |||||
end = expr(state, attr, quote || '[>/\\s]', valueStart) | |||||
// adjust the bounds of the value and save its content | |||||
return Object.assign(attr, { | |||||
value: getChunk(data, valueStart, end), | |||||
valueStart, | |||||
end: quote ? ++end : end | |||||
}) | |||||
} | |||||
return attr | |||||
} | |||||
/** | |||||
* Parse expression names <a {href}> | |||||
* @param {ParserStore} state - Parser state | |||||
* @param {Object} attr - attribute object parsed | |||||
* @returns {Object} normalized attribute object | |||||
*/ | |||||
function parseSpreadAttribute(state, attr) { | |||||
const end = getAttributeEnd(state, attr) | |||||
return { | |||||
[IS_SPREAD]: true, | |||||
start: attr.start, | |||||
expressions: attr.expressions.map(expr => Object.assign(expr, { | |||||
text: expr.text.replace(SPREAD_OPERATOR, '').trim() | |||||
})), | |||||
end: end | |||||
} | |||||
} | |||||
/** | |||||
* Parse expression names <a {href}> | |||||
* @param {ParserStore} state - Parser state | |||||
* @param {Object} attr - attribute object parsed | |||||
* @returns {Object} normalized attribute object | |||||
*/ | |||||
function parseExpressionNameAttribute(state, attr) { | |||||
const end = getAttributeEnd(state, attr) | |||||
return { | |||||
start: attr.start, | |||||
name: attr.expressions[0].text.trim(), | |||||
expressions: attr.expressions, | |||||
end: end | |||||
} | |||||
} | |||||
/** | |||||
* Parse the attribute values normalising the quotes | |||||
* @param {ParserStore} state - Parser state | |||||
* @param {Array} match - results of the attributes regex | |||||
* @param {number} start - attribute start position | |||||
* @param {number} end - attribute end position | |||||
* @param {boolean} isExpressionName - true if the attribute name is an expression | |||||
* @returns {Object} attribute object | |||||
*/ | |||||
function parseAttribute(state, match, start, end, isExpressionName) { | |||||
const attr = { | |||||
name: match[1], | |||||
value: '', | |||||
start, | |||||
end | |||||
} | |||||
const quote = match[2] // first letter of value or nothing | |||||
switch (true) { | |||||
case isSpreadAttribute(attr.name): | |||||
return parseSpreadAttribute(state, attr) | |||||
case isExpressionName === true: | |||||
return parseExpressionNameAttribute(state, attr) | |||||
default: | |||||
return parseNomalAttribute(state, attr, quote) | |||||
} | |||||
} |
@ -0,0 +1,43 @@ | |||||
import {COMMENT, TEXT} from '../node-types' | |||||
import flush from '../utils/flush-parser-state' | |||||
import panic from '../utils/panic' | |||||
import {unclosedComment} from '../messages' | |||||
/** | |||||
* Parses comments in long or short form | |||||
* (any DOCTYPE & CDATA blocks are parsed as comments). | |||||
* | |||||
* @param {ParserState} state - Parser state | |||||
* @param {string} data - Buffer to parse | |||||
* @param {number} start - Position of the '<!' sequence | |||||
* @returns {number} node type id | |||||
* @private | |||||
*/ | |||||
export default function comment(state, data, start) { | |||||
const pos = start + 2 // skip '<!' | |||||
const str = data.substr(pos, 2) === '--' ? '-->' : '>' | |||||
const end = data.indexOf(str, pos) | |||||
if (end < 0) { | |||||
panic(data, unclosedComment, start) | |||||
} | |||||
pushComment(state, start, end + str.length) | |||||
return TEXT | |||||
} | |||||
/** | |||||
* Parse a comment. | |||||
* | |||||
* @param {ParserState} state - Current parser state | |||||
* @param {number} start - Start position of the tag | |||||
* @param {number} end - Ending position (last char of the tag) | |||||
* @returns {undefined} void function | |||||
* @private | |||||
*/ | |||||
export function pushComment(state, start, end) { | |||||
flush(state) | |||||
state.pos = end | |||||
if (state.options.comments === true) { | |||||
state.last = { type: COMMENT, start, end } | |||||
} | |||||
} |
@ -0,0 +1,100 @@ | |||||
import escapeStr from '../utils/escape-str' | |||||
import exprExtr from '../utils/expr-extr' | |||||
import panic from '../utils/panic' | |||||
import pushText from '../utils/push-text' | |||||
import {unexpectedEndOfFile} from '../messages' | |||||
/** | |||||
* Find the end of the attribute value or text node | |||||
* Extract expressions. | |||||
* Detect if value have escaped brackets. | |||||
* | |||||
* @param {ParserState} state - Parser state | |||||
* @param {HasExpr} node - Node if attr, info if text | |||||
* @param {string} endingChars - Ends the value or text | |||||
* @param {number} start - Starting position | |||||
* @returns {number} Ending position | |||||
* @private | |||||
*/ | |||||
export default function expr(state, node, endingChars, start) { | |||||
const re = b0re(state, endingChars) | |||||
re.lastIndex = start // reset re position | |||||
const { unescape, expressions, end } = parseExpressions(state, re) | |||||
if (node) { | |||||
if (unescape) { | |||||
node.unescape = unescape | |||||
} | |||||
if (expressions.length) { | |||||
node.expressions = expressions | |||||
} | |||||
} else { | |||||
pushText(state, start, end, {expressions, unescape}) | |||||
} | |||||
return end | |||||
} | |||||
/** | |||||
* Parse a text chunk finding all the expressions in it | |||||
* @param {ParserState} state - Parser state | |||||
* @param {RegExp} re - regex to match the expressions contents | |||||
* @returns {Object} result containing the expression found, the string to unescape and the end position | |||||
*/ | |||||
function parseExpressions(state, re) { | |||||
const { data, options } = state | |||||
const { brackets } = options | |||||
const expressions = [] | |||||
let unescape, pos, match | |||||
// Anything captured in $1 (closing quote or character) ends the loop... | |||||
while ((match = re.exec(data)) && !match[1]) { | |||||
// ...else, we have an opening bracket and maybe an expression. | |||||
pos = match.index | |||||
if (data[pos - 1] === '\\') { | |||||
unescape = match[0] // it is an escaped opening brace | |||||
} else { | |||||
const tmpExpr = exprExtr(data, pos, brackets) | |||||
if (tmpExpr) { | |||||
expressions.push(tmpExpr) | |||||
re.lastIndex = tmpExpr.end | |||||
} | |||||
} | |||||
} | |||||
// Even for text, the parser needs match a closing char | |||||
if (!match) { | |||||
panic(data, unexpectedEndOfFile, pos) | |||||
} | |||||
return { | |||||
unescape, | |||||
expressions, | |||||
end: match.index | |||||
} | |||||
} | |||||
/** | |||||
* Creates a regex for the given string and the left bracket. | |||||
* The string is captured in $1. | |||||
* | |||||
* @param {ParserState} state - Parser state | |||||
* @param {string} str - String to search | |||||
* @returns {RegExp} Resulting regex. | |||||
* @private | |||||
*/ | |||||
function b0re(state, str) { | |||||
const { brackets } = state.options | |||||
const re = state.regexCache[str] | |||||
if (re) return re | |||||
const b0 = escapeStr(brackets[0]) | |||||
// cache the regex extending the regexCache object | |||||
Object.assign(state.regexCache, { [str]: new RegExp(`(${str})|${b0}`, 'g') }) | |||||
return state.regexCache[str] | |||||
} |
@ -0,0 +1,49 @@ | |||||
import {ATTR, TEXT} from '../node-types' | |||||
import {RE_SCRYLE, TAG_2C, TAG_NAME} from '../regex' | |||||
import comment from './comment' | |||||
import execFromPos from '../utils/exec-from-pos' | |||||
import pushTag from '../utils/push-tag' | |||||
import pushText from '../utils/push-text' | |||||
/** | |||||
* Parse the tag following a '<' character, or delegate to other parser | |||||
* if an invalid tag name is found. | |||||
* | |||||
* @param {ParserState} state - Parser state | |||||
* @returns {number} New parser mode | |||||
* @private | |||||
*/ | |||||
export default function tag(state) { | |||||
const { pos, data } = state // pos of the char following '<' | |||||
const start = pos - 1 // pos of '<' | |||||
const str = data.substr(pos, 2) // first two chars following '<' | |||||
switch (true) { | |||||
case str[0] === '!': | |||||
return comment(state, data, start) | |||||
case TAG_2C.test(str): | |||||
return parseTag(state, start) | |||||
default: | |||||
return pushText(state, start, pos) // pushes the '<' as text | |||||
} | |||||
} | |||||
function parseTag(state, start) { | |||||
const { data, pos } = state | |||||
const re = TAG_NAME // (\/?[^\s>/]+)\s*(>)? g | |||||
const match = execFromPos(re, pos, data) | |||||
const end = re.lastIndex | |||||
const name = match[1].toLowerCase() // $1: tag name including any '/' | |||||
// script/style block is parsed as another tag to extract attributes | |||||
if (name in RE_SCRYLE) { | |||||
state.scryle = name // used by parseText | |||||
} | |||||
pushTag(state, name, start, end) | |||||
// only '>' can ends the tag here, the '/' is handled in parseAttribute | |||||
if (!match[2]) { | |||||
return ATTR | |||||
} | |||||
return TEXT | |||||
} |
@ -0,0 +1,69 @@ | |||||
import {TAG, TEXT} from '../node-types' | |||||
import {RE_SCRYLE} from '../regex' | |||||
import {TEXTAREA_TAG} from '../constants' | |||||
import execFromPos from '../utils/exec-from-pos' | |||||
import expr from './expression' | |||||
import panic from '../utils/panic' | |||||
import pushTag from '../utils/push-tag' | |||||
import pushText from '../utils/push-text' | |||||
import {unclosedNamedBlock} from '../messages' | |||||
/** | |||||
* Parses regular text and script/style blocks ...scryle for short :-) | |||||
* (the content of script and style is text as well) | |||||
* | |||||
* @param {ParserState} state - Parser state | |||||
* @returns {number} New parser mode. | |||||
* @private | |||||
*/ | |||||
export default function text(state) { | |||||
const { pos, data, scryle } = state | |||||
switch (true) { | |||||
case typeof scryle === 'string': { | |||||
const name = scryle | |||||
const re = RE_SCRYLE[name] | |||||
const match = execFromPos(re, pos, data) | |||||
if (!match) { | |||||
panic(data, unclosedNamedBlock.replace('%1', name), pos - 1) | |||||
} | |||||
const start = match.index | |||||
const end = re.lastIndex | |||||
state.scryle = null // reset the script/style flag now | |||||
// write the tag content, if any | |||||
if (start > pos) { | |||||
parseSpecialTagsContent(state, name, match) | |||||
} | |||||
// now the closing tag, either </script> or </style> | |||||
pushTag(state, `/${name}`, start, end) | |||||
break | |||||
} | |||||
case data[pos] === '<': | |||||
state.pos++ | |||||
return TAG | |||||
default: | |||||
expr(state, null, '<', pos) | |||||
} | |||||
return TEXT | |||||
} | |||||
/** | |||||
* Parse the text content depending on the name | |||||
* @param {ParserState} state - Parser state | |||||
* @param {string} name - one of the tags matched by the RE_SCRYLE regex | |||||
* @param {Array} match - result of the regex matching the content of the parsed tag | |||||
* @returns {undefined} void function | |||||
*/ | |||||
function parseSpecialTagsContent(state, name, match) { | |||||
const { pos } = state | |||||
const start = match.index | |||||
if (name === TEXTAREA_TAG) { | |||||
expr(state, null, match[0], pos) | |||||
} else { | |||||
pushText(state, pos, start) | |||||
} | |||||
} |
@ -0,0 +1,41 @@ | |||||
/** | |||||
* Matches the start of valid tags names; used with the first 2 chars after the `'<'`. | |||||
* @const | |||||
* @private | |||||
*/ | |||||
export const TAG_2C = /^(?:\/[a-zA-Z]|[a-zA-Z][^\s>/]?)/ | |||||
/** | |||||
* Matches valid tags names AFTER the validation with `TAG_2C`. | |||||
* $1: tag name including any `'/'`, $2: non self-closing brace (`>`) w/o attributes. | |||||
* @const | |||||
* @private | |||||
*/ | |||||
export const TAG_NAME = /(\/?[^\s>/]+)\s*(>)?/g | |||||
/** | |||||
* Matches an attribute name-value pair (both can be empty). | |||||
* $1: attribute name, $2: value including any quotes. | |||||
* @const | |||||
* @private | |||||
*/ | |||||
export const ATTR_START = /(\S[^>/=\s]*)(?:\s*=\s*([^>/])?)?/g | |||||
/** | |||||
* Matches the spread operator | |||||
* it will be used for the spread attributes | |||||
* @type {RegExp} | |||||
*/ | |||||
export const SPREAD_OPERATOR = /\.\.\./ | |||||
/** | |||||
* Matches the closing tag of a `script` and `style` block. | |||||
* Used by parseText fo find the end of the block. | |||||
* @const | |||||
* @private | |||||
*/ | |||||
export const RE_SCRYLE = { | |||||
script: /<\/script\s*>/gi, | |||||
style: /<\/style\s*>/gi, | |||||
textarea: /<\/textarea\s*>/gi | |||||
} | |||||
// Do not touch text content inside this tags | |||||
export const RAW_TAGS = /^\/?(?:pre|textarea)$/ |
@ -0,0 +1,238 @@ | |||||
/*--------------------------------------------------------------------- | |||||
* Tree builder for the riot tag parser. | |||||
* | |||||
* The output has a root property and separate arrays for `html`, `css`, | |||||
* and `js` tags. | |||||
* | |||||
* The root tag is included as first element in the `html` array. | |||||
* Script tags marked with "defer" are included in `html` instead `js`. | |||||
* | |||||
* - Mark SVG tags | |||||
* - Mark raw tags | |||||
* - Mark void tags | |||||
* - Split prefixes from expressions | |||||
* - Unescape escaped brackets and escape EOLs and backslashes | |||||
* - Compact whitespace (option `compact`) for non-raw tags | |||||
* - Create an array `parts` for text nodes and attributes | |||||
* | |||||
* Throws on unclosed tags or closing tags without start tag. | |||||
* Selfclosing and void tags has no nodes[] property. | |||||
*/ | |||||
import { | |||||
CSS_OUTPUT_NAME, | |||||
IS_RAW, | |||||
IS_SELF_CLOSING, | |||||
IS_VOID, | |||||
JAVASCRIPT_OUTPUT_NAME, | |||||
JAVASCRIPT_TAG, | |||||
STYLE_TAG, | |||||
TEMPLATE_OUTPUT_NAME | |||||
} from './constants' | |||||
import {TAG, TEXT} from './node-types' | |||||
import {RAW_TAGS} from './regex' | |||||
import {duplicatedNamedTag} from './messages' | |||||
import panic from './utils/panic' | |||||
/** | |||||
* Escape the carriage return and the line feed from a string | |||||
* @param {string} string - input string | |||||
* @returns {string} output string escaped | |||||
*/ | |||||
function escapeReturn(string) { | |||||
return string | |||||
.replace(/\r/g, '\\r') | |||||
.replace(/\n/g, '\\n') | |||||
} | |||||
/** | |||||
* Escape double slashes in a string | |||||
* @param {string} string - input string | |||||
* @returns {string} output string escaped | |||||
*/ | |||||
function escapeSlashes(string) { | |||||
return string.replace(/\\/g, '\\\\') | |||||
} | |||||
/** | |||||
* Replace the multiple spaces with only one | |||||
* @param {string} string - input string | |||||
* @returns {string} string without trailing spaces | |||||
*/ | |||||
function cleanSpaces(string) { | |||||
return string.replace(/\s+/g, ' ') | |||||
} | |||||
const TREE_BUILDER_STRUCT = Object.seal({ | |||||
get() { | |||||
const store = this.store | |||||
// The real root tag is in store.root.nodes[0] | |||||
return { | |||||
[TEMPLATE_OUTPUT_NAME]: store.root.nodes[0], | |||||
[CSS_OUTPUT_NAME]: store[STYLE_TAG], | |||||
[JAVASCRIPT_OUTPUT_NAME]: store[JAVASCRIPT_TAG] | |||||
} | |||||
}, | |||||
/** | |||||
* Process the current tag or text. | |||||
* @param {Object} node - Raw pseudo-node from the parser | |||||
* @returns {undefined} void function | |||||
*/ | |||||
push(node) { | |||||
const store = this.store | |||||
switch (node.type) { | |||||
case TEXT: | |||||
this.pushText(store, node) | |||||
break | |||||
case TAG: { | |||||
const name = node.name | |||||
const closingTagChar = '/' | |||||
const [firstChar] = name | |||||
if (firstChar === closingTagChar && !node.isVoid) { | |||||
this.closeTag(store, node, name) | |||||
} else if (firstChar !== closingTagChar) { | |||||
this.openTag(store, node) | |||||
} | |||||
break | |||||
} | |||||
} | |||||
}, | |||||
closeTag(store, node) { | |||||
const last = store.scryle || store.last | |||||
last.end = node.end | |||||
if (store.scryle) { | |||||
store.scryle = null | |||||
} else { | |||||
store.last = store.stack.pop() | |||||
} | |||||
}, | |||||
openTag(store, node) { | |||||
const name = node.name | |||||
const attrs = node.attributes | |||||
if ([JAVASCRIPT_TAG, STYLE_TAG].includes(name)) { | |||||
// Only accept one of each | |||||
if (store[name]) { | |||||
panic(this.store.data, duplicatedNamedTag.replace('%1', name), node.start) | |||||
} | |||||
store[name] = node | |||||
store.scryle = store[name] | |||||
} else { | |||||
// store.last holds the last tag pushed in the stack and this are | |||||
// non-void, non-empty tags, so we are sure the `lastTag` here | |||||
// have a `nodes` property. | |||||
const lastTag = store.last | |||||
const newNode = node | |||||
lastTag.nodes.push(newNode) | |||||
if (lastTag[IS_RAW] || RAW_TAGS.test(name)) { | |||||
node[IS_RAW] = true | |||||
} | |||||
if (!node[IS_SELF_CLOSING] && !node[IS_VOID]) { | |||||
store.stack.push(lastTag) | |||||
newNode.nodes = [] | |||||
store.last = newNode | |||||
} | |||||
} | |||||
if (attrs) { | |||||
this.attrs(attrs) | |||||
} | |||||
}, | |||||
attrs(attributes) { | |||||
attributes.forEach(attr => { | |||||
if (attr.value) { | |||||
this.split(attr, attr.value, attr.valueStart, true) | |||||
} | |||||
}) | |||||
}, | |||||
pushText(store, node) { | |||||
const text = node.text | |||||
const empty = !/\S/.test(text) | |||||
const scryle = store.scryle | |||||
if (!scryle) { | |||||
// store.last always have a nodes property | |||||
const parent = store.last | |||||
const pack = this.compact && !parent[IS_RAW] | |||||
if (pack && empty) { | |||||
return | |||||
} | |||||
this.split(node, text, node.start, pack) | |||||
parent.nodes.push(node) | |||||
} else if (!empty) { | |||||
scryle.text = node | |||||
} | |||||
}, | |||||
split(node, source, start, pack) { | |||||
const expressions = node.expressions | |||||
const parts = [] | |||||
if (expressions) { | |||||
let pos = 0 | |||||
expressions.forEach(expr => { | |||||
const text = source.slice(pos, expr.start - start) | |||||
const code = expr.text | |||||
parts.push(this.sanitise(node, text, pack), escapeReturn(escapeSlashes(code).trim())) | |||||
pos = expr.end - start | |||||
}) | |||||
if (pos < node.end) { | |||||
parts.push(this.sanitise(node, source.slice(pos), pack)) | |||||
} | |||||
} else { | |||||
parts[0] = this.sanitise(node, source, pack) | |||||
} | |||||
node.parts = parts.filter(p => p) // remove the empty strings | |||||
}, | |||||
// unescape escaped brackets and split prefixes of expressions | |||||
sanitise(node, text, pack) { | |||||
let rep = node.unescape | |||||
if (rep) { | |||||
let idx = 0 | |||||
rep = `\\${rep}` | |||||
while ((idx = text.indexOf(rep, idx)) !== -1) { | |||||
text = text.substr(0, idx) + text.substr(idx + 1) | |||||
idx++ | |||||
} | |||||
} | |||||
text = escapeSlashes(text) | |||||
return pack ? cleanSpaces(text) : escapeReturn(text) | |||||
} | |||||
}) | |||||
export default function createTreeBuilder(data, options) { | |||||
const root = { | |||||
type: TAG, | |||||
name: '', | |||||
start: 0, | |||||
end: 0, | |||||
nodes: [] | |||||
} | |||||
return Object.assign(Object.create(TREE_BUILDER_STRUCT), { | |||||
compact: options.compact !== false, | |||||
store: { | |||||
last: root, | |||||
stack: [], | |||||
scryle: null, | |||||
root, | |||||
style: null, | |||||
script: null, | |||||
data | |||||
} | |||||
}) | |||||
} |
@ -0,0 +1,11 @@ | |||||
/** | |||||
* Add an item into a collection, if the collection is not an array | |||||
* we create one and add the item to it | |||||
* @param {Array} collection - target collection | |||||
* @param {*} item - item to add to the collection | |||||
* @returns {Array} array containing the new item added to it | |||||
*/ | |||||
export default function addToCollection(collection = [], item) { | |||||
collection.push(item) | |||||
return collection | |||||
} |
@ -0,0 +1,7 @@ | |||||
/** | |||||
* Escape special characters in a given string, in preparation to create a regex. | |||||
* | |||||
* @param {string} str - Raw string | |||||
* @returns {string} Escaped string. | |||||
*/ | |||||
export default (str) => str.replace(/(?=[-[\](){^*+?.$|\\])/g, '\\') |
@ -0,0 +1,11 @@ | |||||
/** | |||||
* Run RegExp.exec starting from a specific position | |||||
* @param {RegExp} re - regex | |||||
* @param {number} pos - last index position | |||||
* @param {string} string - regex target | |||||
* @returns {Array} regex result | |||||
*/ | |||||
export default function execFromPos(re, pos, string) { | |||||
re.lastIndex = pos | |||||
return re.exec(string) | |||||
} |
@ -0,0 +1,139 @@ | |||||
/* | |||||
* Mini-parser for expressions. | |||||
* The main pourpose of this module is to find the end of an expression | |||||
* and return its text without the enclosing brackets. | |||||
* Does not works with comments, but supports ES6 template strings. | |||||
*/ | |||||
import skipES6TL, {$_ES6_BQ} from './skip-es6-tl' | |||||
import {unclosedExpression, unexpectedCharInExpression} from '../messages' | |||||
import escapeStr from './escape-str' | |||||
import panic from './panic' | |||||
import skipRegex from './skip-regex' | |||||
/** | |||||
* @exports exprExtr | |||||
*/ | |||||
const S_SQ_STR = /'[^'\n\r\\]*(?:\\(?:\r\n?|[\S\s])[^'\n\r\\]*)*'/.source | |||||
/** | |||||
* Matches double quoted JS strings taking care about nested quotes | |||||
* and EOLs (escaped EOLs are Ok). | |||||
* | |||||
* @const | |||||
* @private | |||||
*/ | |||||
const S_STRING = `${S_SQ_STR}|${S_SQ_STR.replace(/'/g, '"')}` | |||||
/** | |||||
* Regex cache | |||||
* | |||||
* @type {Object.<string, RegExp>} | |||||
* @const | |||||
* @private | |||||
*/ | |||||
const reBr = {} | |||||
/** | |||||
* Makes an optimal regex that matches quoted strings, brackets, backquotes | |||||
* and the closing brackets of an expression. | |||||
* | |||||
* @param {string} b - Closing brackets | |||||
* @returns {RegExp} - optimized regex | |||||
*/ | |||||
function _regex(b) { | |||||
let re = reBr[b] | |||||
if (!re) { | |||||
let s = escapeStr(b) | |||||
if (b.length > 1) { | |||||
s = `${s}|[` | |||||
} else { | |||||
s = /[{}[\]()]/.test(b) ? '[' : `[${s}` | |||||
} | |||||
reBr[b] = re = new RegExp(`${S_STRING}|${s}\`/\\{}[\\]()]`, 'g') | |||||
} | |||||
return re | |||||
} | |||||
/** | |||||
* Update the scopes stack removing or adding closures to it | |||||
* @param {Array} stack - array stacking the expression closures | |||||
* @param {string} char - current char to add or remove from the stack | |||||
* @param {string} idx - matching index | |||||
* @param {string} code - expression code | |||||
* @returns {Object} result | |||||
* @returns {Object} result.char - either the char received or the closing braces | |||||
* @returns {Object} result.index - either a new index to skip part of the source code, | |||||
* or 0 to keep from parsing from the old position | |||||
*/ | |||||
function updateStack(stack, char, idx, code) { | |||||
let index = 0 | |||||
switch (char) { | |||||
case '[': | |||||
case '(': | |||||
case '{': | |||||
stack.push(char === '[' ? ']' : char === '(' ? ')' : '}') | |||||
break | |||||
case ')': | |||||
case ']': | |||||
case '}': | |||||
if (char !== stack.pop()) { | |||||
panic(code, unexpectedCharInExpression.replace('%1', char), index) | |||||
} | |||||
if (char === '}' && stack[stack.length - 1] === $_ES6_BQ) { | |||||
char = stack.pop() | |||||
} | |||||
index = idx + 1 | |||||
break | |||||
case '/': | |||||
index = skipRegex(code, idx) | |||||
} | |||||
return { char, index } | |||||
} | |||||
/** | |||||
* Parses the code string searching the end of the expression. | |||||
* It skips braces, quoted strings, regexes, and ES6 template literals. | |||||
* | |||||
* @function exprExtr | |||||
* @param {string} code - Buffer to parse | |||||
* @param {number} start - Position of the opening brace | |||||
* @param {[string,string]} bp - Brackets pair | |||||
* @returns {Object} Expression's end (after the closing brace) or -1 | |||||
* if it is not an expr. | |||||
*/ | |||||
export default function exprExtr(code, start, bp) { | |||||
const [openingBraces, closingBraces] = bp | |||||
const offset = start + openingBraces.length // skips the opening brace | |||||
const stack = [] // expected closing braces ('`' for ES6 TL) | |||||
const re = _regex(closingBraces) | |||||
re.lastIndex = offset // begining of the expression | |||||
let end | |||||
let match | |||||
while (match = re.exec(code)) { // eslint-disable-line | |||||
const idx = match.index | |||||
const str = match[0] | |||||
end = re.lastIndex | |||||
// end the iteration | |||||
if (str === closingBraces && !stack.length) { | |||||
return { | |||||
text: code.slice(offset, idx), | |||||
start, | |||||
end | |||||
} | |||||
} | |||||
const { char, index } = updateStack(stack, str[0], idx, code) | |||||
// update the end value depending on the new index received | |||||
end = index || end | |||||
// update the regex last index | |||||
re.lastIndex = char === $_ES6_BQ ? skipES6TL(code, end, stack) : end | |||||
} | |||||
if (stack.length) { | |||||
panic(code, unclosedExpression, end) | |||||
} | |||||
} |
@ -0,0 +1,14 @@ | |||||
/** | |||||
* Outputs the last parsed node. Can be used with a builder too. | |||||
* | |||||
* @param {ParserStore} store - Parsing store | |||||
* @returns {undefined} void function | |||||
* @private | |||||
*/ | |||||
export default function flush(store) { | |||||
const last = store.last | |||||
store.last = null | |||||
if (last && store.root) { | |||||
store.builder.push(last) | |||||
} | |||||
} |
@ -0,0 +1,12 @@ | |||||
export default function formatError(data, message, pos) { | |||||
if (!pos) { | |||||
pos = data.length | |||||
} | |||||
// count unix/mac/win eols | |||||
const line = (data.slice(0, pos).match(/\r\n?|\n/g) || '').length + 1 | |||||
let col = 0 | |||||
while (--pos >= 0 && !/[\r\n]/.test(data[pos])) { | |||||
++col | |||||
} | |||||
return `[${line},${col}]: ${message}` | |||||
} |
@ -0,0 +1,11 @@ | |||||
/** | |||||
* Get the code chunks from start and end range | |||||
* @param {string} source - source code | |||||
* @param {number} start - Start position of the chunk we want to extract | |||||
* @param {number} end - Ending position of the chunk we need | |||||
* @returns {string} chunk of code extracted from the source code received | |||||
* @private | |||||
*/ | |||||
export default function getChunk(source, start, end) { | |||||
return source.slice(start, end) | |||||
} |
@ -0,0 +1,18 @@ | |||||
/** | |||||
* Memoization function | |||||
* @param {Function} fn - function to memoize | |||||
* @returns {*} return of the function to memoize | |||||
*/ | |||||
export default function memoize(fn) { | |||||
const cache = new WeakMap() | |||||
return (...args) => { | |||||
if (cache.has(args[0])) return cache.get(args[0]) | |||||
const ret = fn(...args) | |||||
cache.set(args[0], ret) | |||||
return ret | |||||
} | |||||
} |
@ -0,0 +1,15 @@ | |||||
import formatError from './format-error' | |||||
/** | |||||
* Custom error handler can be implemented replacing this method. | |||||
* The `state` object includes the buffer (`data`) | |||||
* The error position (`loc`) contains line (base 1) and col (base 0). | |||||
* @param {string} data - string containing the error | |||||
* @param {string} msg - Error message | |||||
* @param {number} pos - Position of the error | |||||
* @returns {undefined} throw an exception error | |||||
*/ | |||||
export default function panic(data, msg, pos) { | |||||
const message = formatError(data, msg, pos) | |||||
throw new Error(message) | |||||
} |
@ -0,0 +1,48 @@ | |||||
import { | |||||
IS_CUSTOM, | |||||
IS_VOID | |||||
} from '../constants' | |||||
import {isCustom, isVoid} from 'dom-nodes' | |||||
import {TAG} from '../node-types' | |||||
import flush from './flush-parser-state' | |||||
/** | |||||
* Pushes a new *tag* and set `last` to this, so any attributes | |||||
* will be included on this and shifts the `end`. | |||||
* | |||||
* @param {ParserState} state - Current parser state | |||||
* @param {string} name - Name of the node including any slash | |||||
* @param {number} start - Start position of the tag | |||||
* @param {number} end - Ending position (last char of the tag + 1) | |||||
* @returns {undefined} - void function | |||||
* @private | |||||
*/ | |||||
export default function pushTag(state, name, start, end) { | |||||
const root = state.root | |||||
const last = { type: TAG, name, start, end } | |||||
if (isCustom(name)) { | |||||
last[IS_CUSTOM] = true | |||||
} | |||||
if (isVoid(name)) { | |||||
last[IS_VOID] = true | |||||
} | |||||
state.pos = end | |||||
if (root) { | |||||
if (name === root.name) { | |||||
state.count++ | |||||
} else if (name === root.close) { | |||||
state.count-- | |||||
} | |||||
flush(state) | |||||
} else { | |||||
// start with root (keep ref to output) | |||||
state.root = { name: last.name, close: `/${name}` } | |||||
state.count = 1 | |||||
} | |||||
state.last = last | |||||
} |
@ -0,0 +1,42 @@ | |||||
import {TEXT} from '../node-types' | |||||
import flush from './flush-parser-state' | |||||
import getChunk from './get-chunk' | |||||
/** | |||||
* states text in the last text node, or creates a new one if needed. | |||||
* | |||||
* @param {ParserState} state - Current parser state | |||||
* @param {number} start - Start position of the tag | |||||
* @param {number} end - Ending position (last char of the tag) | |||||
* @param {Object} extra - extra properties to add to the text node | |||||
* @param {RawExpr[]} extra.expressions - Found expressions | |||||
* @param {string} extra.unescape - Brackets to unescape | |||||
* @returns {undefined} - void function | |||||
* @private | |||||
*/ | |||||
export default function pushText(state, start, end, extra = {}) { | |||||
const text = getChunk(state.data, start, end) | |||||
const expressions = extra.expressions | |||||
const unescape = extra.unescape | |||||
let q = state.last | |||||
state.pos = end | |||||
if (q && q.type === TEXT) { | |||||
q.text += text | |||||
q.end = end | |||||
} else { | |||||
flush(state) | |||||
state.last = q = { type: TEXT, text, start, end } | |||||
} | |||||
if (expressions && expressions.length) { | |||||
q.expressions = (q.expressions || []).concat(expressions) | |||||
} | |||||
if (unescape) { | |||||
q.unescape = unescape | |||||
} | |||||
return TEXT | |||||
} |
@ -0,0 +1,33 @@ | |||||
import formatError from './format-error' | |||||
import {unclosedTemplateLiteral} from '../messages' | |||||
export const $_ES6_BQ = '`' | |||||
/** | |||||
* Searches the next backquote that signals the end of the ES6 Template Literal | |||||
* or the "${" sequence that starts a JS expression, skipping any escaped | |||||
* character. | |||||
* | |||||
* @param {string} code - Whole code | |||||
* @param {number} pos - The start position of the template | |||||
* @param {string[]} stack - To save nested ES6 TL count | |||||
* @returns {number} The end of the string (-1 if not found) | |||||
*/ | |||||
export default function skipES6TL(code, pos, stack) { | |||||
// we are in the char following the backquote (`), | |||||
// find the next unescaped backquote or the sequence "${" | |||||
const re = /[`$\\]/g | |||||
let c | |||||
while (re.lastIndex = pos, re.exec(code)) { | |||||
pos = re.lastIndex | |||||
c = code[pos - 1] | |||||
if (c === '`') { | |||||
return pos | |||||
} | |||||
if (c === '$' && code[pos++] === '{') { | |||||
stack.push($_ES6_BQ, '}') | |||||
return pos | |||||
} | |||||
// else this is an escaped char | |||||
} | |||||
throw formatError(code, unclosedTemplateLiteral, pos) | |||||
} |
@ -0,0 +1,116 @@ | |||||
// forked from https://github.com/aMarCruz/skip-regex | |||||
// safe characters to precced a regex (including `=>`, `**`, and `...`) | |||||
const beforeReChars = '[{(,;:?=|&!^~>%*/' | |||||
const beforeReSign = `${beforeReChars}+-` | |||||
// keyword that can preceed a regex (`in` is handled as special case) | |||||
const beforeReWords = [ | |||||
'case', | |||||
'default', | |||||
'do', | |||||
'else', | |||||
'in', | |||||
'instanceof', | |||||
'prefix', | |||||
'return', | |||||
'typeof', | |||||
'void', | |||||
'yield' | |||||
] | |||||
// Last chars of all the beforeReWords elements to speed up the process. | |||||
const wordsEndChar = beforeReWords.reduce((s, w) => s + w.slice(-1), '') | |||||
// Matches literal regex from the start of the buffer. | |||||
// The buffer to search must not include line-endings. | |||||
const RE_LIT_REGEX = /^\/(?=[^*>/])[^[/\\]*(?:(?:\\.|\[(?:\\.|[^\]\\]*)*\])[^[\\/]*)*?\/[gimuy]*/ | |||||
// Valid characters for JavaScript variable names and literal numbers. | |||||
const RE_JS_VCHAR = /[$\w]/ | |||||
// Match dot characters that could be part of tricky regex | |||||
const RE_DOT_CHAR = /.*/g | |||||
/** | |||||
* Searches the position of the previous non-blank character inside `code`, | |||||
* starting with `pos - 1`. | |||||
* | |||||
* @param {string} code - Buffer to search | |||||
* @param {number} pos - Starting position | |||||
* @returns {number} Position of the first non-blank character to the left. | |||||
* @private | |||||
*/ | |||||
function _prev(code, pos) { | |||||
while (--pos >= 0 && /\s/.test(code[pos])); | |||||
return pos | |||||
} | |||||
/** | |||||
* Check if the character in the `start` position within `code` can be a regex | |||||
* and returns the position following this regex or `start+1` if this is not | |||||
* one. | |||||
* | |||||
* NOTE: Ensure `start` points to a slash (this is not checked). | |||||
* | |||||
* @function skipRegex | |||||
* @param {string} code - Buffer to test in | |||||
* @param {number} start - Position the first slash inside `code` | |||||
* @returns {number} Position of the char following the regex. | |||||
* | |||||
*/ | |||||
/* istanbul ignore next */ | |||||
export default function skipRegex(code, start) { | |||||
let pos = RE_DOT_CHAR.lastIndex = start++ | |||||
// `exec()` will extract from the slash to the end of the line | |||||
// and the chained `match()` will match the possible regex. | |||||
const match = (RE_DOT_CHAR.exec(code) || ' ')[0].match(RE_LIT_REGEX) | |||||
if (match) { | |||||
const next = pos + match[0].length // result comes from `re.match` | |||||
pos = _prev(code, pos) | |||||
let c = code[pos] | |||||
// start of buffer or safe prefix? | |||||
if (pos < 0 || beforeReChars.includes(c)) { | |||||
return next | |||||
} | |||||
// from here, `pos` is >= 0 and `c` is code[pos] | |||||
if (c === '.') { | |||||
// can be `...` or something silly like 5./2 | |||||
if (code[pos - 1] === '.') { | |||||
start = next | |||||
} | |||||
} else { | |||||
if (c === '+' || c === '-') { | |||||
// tricky case | |||||
if (code[--pos] !== c || // if have a single operator or | |||||
(pos = _prev(code, pos)) < 0 || // ...have `++` and no previous token | |||||
beforeReSign.includes(c = code[pos])) { | |||||
return next // ...this is a regex | |||||
} | |||||
} | |||||
if (wordsEndChar.includes(c)) { // looks like a keyword? | |||||
const end = pos + 1 | |||||
// get the complete (previous) keyword | |||||
while (--pos >= 0 && RE_JS_VCHAR.test(code[pos])); | |||||
// it is in the allowed keywords list? | |||||
if (beforeReWords.includes(code.slice(pos + 1, end))) { | |||||
start = next | |||||
} | |||||
} | |||||
} | |||||
} | |||||
return start | |||||
} |
@ -0,0 +1,15 @@ | |||||
ISC License | |||||
Copyright (c) 2018, Andrea Giammarchi, @WebReflection | |||||
Permission to use, copy, modify, and/or distribute this software for any | |||||
purpose with or without fee is hereby granted, provided that the above | |||||
copyright notice and this permission notice appear in all copies. | |||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | |||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE | |||||
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |||||
PERFORMANCE OF THIS SOFTWARE. |
@ -0,0 +1,15 @@ | |||||
# Essential Map | |||||
[![Build Status](https://travis-ci.com/ungap/essential-map.svg?branch=master)](https://travis-ci.com/ungap/essential-map) [![Coverage Status](https://coveralls.io/repos/github/ungap/essential-map/badge.svg?branch=master)](https://coveralls.io/github/ungap/essential-map?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/ungap/essential-map.svg)](https://greenkeeper.io/) ![WebReflection status](https://offline.report/status/webreflection.svg) | |||||
An essential [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) poorlyfill for legacy browsers. | |||||
Only methods are `delete`, `forEach`, `get`, `has`, and `set`. | |||||
* CDN via https://unpkg.com/@ungap/essential-map | |||||
* ESM via `import Map from '@ungap/essential-map'` | |||||
* CJS via `const Map = require('@ungap/essential-map')` | |||||
Compatible down to IE9, works well with ES5 shim upfront in IE8 (and maybe lower too). | |||||
[Live test](https://ungap.github.io/essential-map/test/) |
@ -0,0 +1,43 @@ | |||||
/*! (c) Andrea Giammarchi - ISC */ | |||||
var self = this || /* istanbul ignore next */ {}; | |||||
try { self.Map = Map; } | |||||
catch (Map) { | |||||
self.Map = function Map() { | |||||
var i = 0; | |||||
var k = []; | |||||
var v = []; | |||||
return { | |||||
delete: function (key) { | |||||
var had = contains(key); | |||||
if (had) { | |||||
k.splice(i, 1); | |||||
v.splice(i, 1); | |||||
} | |||||
return had; | |||||
}, | |||||
forEach: function forEach(callback, context) { | |||||
k.forEach( | |||||
function (key, i) { | |||||
callback.call(context, v[i], key, this); | |||||
}, | |||||
this | |||||
); | |||||
}, | |||||
get: function get(key) { | |||||
return contains(key) ? v[i] : void 0; | |||||
}, | |||||
has: function has(key) { | |||||
return contains(key); | |||||
}, | |||||
set: function set(key, value) { | |||||
v[contains(key) ? i : (k.push(key) - 1)] = value; | |||||
return this; | |||||
} | |||||
}; | |||||
function contains(v) { | |||||
i = k.indexOf(v); | |||||
return -1 < i; | |||||
} | |||||
}; | |||||
} | |||||
module.exports = self.Map; |
@ -0,0 +1,43 @@ | |||||
/*! (c) Andrea Giammarchi - ISC */ | |||||
var self = this || /* istanbul ignore next */ {}; | |||||
try { self.Map = Map; } | |||||
catch (Map) { | |||||
self.Map = function Map() { | |||||
var i = 0; | |||||
var k = []; | |||||
var v = []; | |||||
return { | |||||
delete: function (key) { | |||||
var had = contains(key); | |||||
if (had) { | |||||
k.splice(i, 1); | |||||
v.splice(i, 1); | |||||
} | |||||
return had; | |||||
}, | |||||
forEach: function forEach(callback, context) { | |||||
k.forEach( | |||||
function (key, i) { | |||||
callback.call(context, v[i], key, this); | |||||
}, | |||||
this | |||||
); | |||||
}, | |||||
get: function get(key) { | |||||
return contains(key) ? v[i] : void 0; | |||||
}, | |||||
has: function has(key) { | |||||
return contains(key); | |||||
}, | |||||
set: function set(key, value) { | |||||
v[contains(key) ? i : (k.push(key) - 1)] = value; | |||||
return this; | |||||
} | |||||
}; | |||||
function contains(v) { | |||||
i = k.indexOf(v); | |||||
return -1 < i; | |||||
} | |||||
}; | |||||
} | |||||
export default self.Map; |
@ -0,0 +1,42 @@ | |||||
/*! (c) Andrea Giammarchi - ISC */ | |||||
var self = this || /* istanbul ignore next */ {}; | |||||
try { self.Map = Map; } | |||||
catch (Map) { | |||||
self.Map = function Map() { | |||||
var i = 0; | |||||
var k = []; | |||||
var v = []; | |||||
return { | |||||
delete: function (key) { | |||||
var had = contains(key); | |||||
if (had) { | |||||
k.splice(i, 1); | |||||
v.splice(i, 1); | |||||
} | |||||
return had; | |||||
}, | |||||
forEach: function forEach(callback, context) { | |||||
k.forEach( | |||||
function (key, i) { | |||||
callback.call(context, v[i], key, this); | |||||
}, | |||||
this | |||||
); | |||||
}, | |||||
get: function get(key) { | |||||
return contains(key) ? v[i] : void 0; | |||||
}, | |||||
has: function has(key) { | |||||
return contains(key); | |||||
}, | |||||
set: function set(key, value) { | |||||
v[contains(key) ? i : (k.push(key) - 1)] = value; | |||||
return this; | |||||
} | |||||
}; | |||||
function contains(v) { | |||||
i = k.indexOf(v); | |||||
return -1 < i; | |||||
} | |||||
}; | |||||
} |
@ -0,0 +1,2 @@ | |||||
/*! (c) Andrea Giammarchi - ISC */ | |||||
var self=this||{};try{self.Map=Map}catch(n){self.Map=function(){var r=0,i=[],c=[];return{delete:function(n){var t=e(n);return t&&(i.splice(r,1),c.splice(r,1)),t},forEach:function(r,e){i.forEach(function(n,t){r.call(e,c[t],n,this)},this)},get:function(n){return e(n)?c[r]:void 0},has:function(n){return e(n)},set:function(n,t){return c[e(n)?r:i.push(n)-1]=t,this}};function e(n){return-1<(r=i.indexOf(n))}}} |
@ -0,0 +1,68 @@ | |||||
{ | |||||
"_from": "@ungap/essential-map@^0.2.0", | |||||
"_id": "@ungap/essential-map@0.2.0", | |||||
"_inBundle": false, | |||||
"_integrity": "sha512-PM7rUPhH2IAeDZXGjMi1KwNVW/4b/wxfzlPkZSXacHA5A98n+PdCou+HD2EaeXlO8pY5Tp81Ysg+J2xB7J/mjw==", | |||||
"_location": "/@ungap/essential-map", | |||||
"_phantomChildren": {}, | |||||
"_requested": { | |||||
"type": "range", | |||||
"registry": true, | |||||
"raw": "@ungap/essential-map@^0.2.0", | |||||
"name": "@ungap/essential-map", | |||||
"escapedName": "@ungap%2fessential-map", | |||||
"scope": "@ungap", | |||||
"rawSpec": "^0.2.0", | |||||
"saveSpec": null, | |||||
"fetchSpec": "^0.2.0" | |||||
}, | |||||
"_requiredBy": [ | |||||
"/domdiff" | |||||
], | |||||
"_resolved": "https://registry.npmjs.org/@ungap/essential-map/-/essential-map-0.2.0.tgz", | |||||
"_shasum": "c8721ac3512331c9b80382be99fff1b04eedba1d", | |||||
"_spec": "@ungap/essential-map@^0.2.0", | |||||
"_where": "/home/herrhase/Workspace/tentakelfabrik/tiny-components/tiny-one-page/node_modules/domdiff", | |||||
"author": { | |||||
"name": "Andrea Giammarchi" | |||||
}, | |||||
"bugs": { | |||||
"url": "https://github.com/ungap/essential-map/issues" | |||||
}, | |||||
"bundleDependencies": false, | |||||
"deprecated": false, | |||||
"description": "An essential Map poorlyfill for legacy browsers.", | |||||
"devDependencies": { | |||||
"coveralls": "^3.0.2", | |||||
"istanbul": "^0.4.5", | |||||
"uglify-js": "^3.4.9" | |||||
}, | |||||
"homepage": "https://github.com/ungap/essential-map#readme", | |||||
"keywords": [ | |||||
"Map", | |||||
"essential", | |||||
"poorlyfill", | |||||
"polyfill", | |||||
"legacy", | |||||
"ungap" | |||||
], | |||||
"license": "ISC", | |||||
"main": "cjs/index.js", | |||||
"module": "esm/index.js", | |||||
"name": "@ungap/essential-map", | |||||
"repository": { | |||||
"type": "git", | |||||
"url": "git+https://github.com/ungap/essential-map.git" | |||||
}, | |||||
"scripts": { | |||||
"build": "npm run cjs && npm run esm && npm run min && npm run test && npm run size", | |||||
"cjs": "cp index.js cjs/ && echo 'module.exports = self.Map;' >> cjs/index.js", | |||||
"coveralls": "cat ./coverage/lcov.info | coveralls", | |||||
"esm": "cp index.js esm/ && echo 'export default self.Map;' >> esm/index.js", | |||||
"min": "uglifyjs index.js --support-ie8 --comments=/^!/ -c -m -o min.js", | |||||
"size": "cat index.js | wc -c && cat min.js | wc -c && gzip -c9 min.js | wc -c && cat min.js | brotli | wc -c", | |||||
"test": "istanbul cover test/index.js" | |||||
}, | |||||
"unpkg": "min.js", | |||||
"version": "0.2.0" | |||||
} |
@ -0,0 +1,552 @@ | |||||
## 7.0.0 (2019-08-13) | |||||
### Breaking changes | |||||
Changes the node format for dynamic imports to use the `ImportExpression` node type, as defined in [ESTree](https://github.com/estree/estree/blob/master/es2020.md#importexpression). | |||||
Makes 10 (ES2019) the default value for the `ecmaVersion` option. | |||||
## 6.3.0 (2019-08-12) | |||||
### New features | |||||
`sourceType: "module"` can now be used even when `ecmaVersion` is less than 6, to parse module-style code that otherwise conforms to an older standard. | |||||
## 6.2.1 (2019-07-21) | |||||
### Bug fixes | |||||
Fix bug causing Acorn to treat some characters as identifier characters that shouldn't be treated as such. | |||||
Fix issue where setting the `allowReserved` option to `"never"` allowed reserved words in some circumstances. | |||||
## 6.2.0 (2019-07-04) | |||||
### Bug fixes | |||||
Improve valid assignment checking in `for`/`in` and `for`/`of` loops. | |||||
Disallow binding `let` in patterns. | |||||
### New features | |||||
Support bigint syntax with `ecmaVersion` >= 10. | |||||
Support dynamic `import` syntax with `ecmaVersion` >= 10. | |||||
Upgrade to Unicode version 12. | |||||
## 6.1.1 (2019-02-27) | |||||
### Bug fixes | |||||
Fix bug that caused parsing default exports of with names to fail. | |||||
## 6.1.0 (2019-02-08) | |||||
### Bug fixes | |||||
Fix scope checking when redefining a `var` as a lexical binding. | |||||
### New features | |||||
Split up `parseSubscripts` to use an internal `parseSubscript` method to make it easier to extend with plugins. | |||||
## 6.0.7 (2019-02-04) | |||||
### Bug fixes | |||||
Check that exported bindings are defined. | |||||
Don't treat `\u180e` as a whitespace character. | |||||
Check for duplicate parameter names in methods. | |||||
Don't allow shorthand properties when they are generators or async methods. | |||||
Forbid binding `await` in async arrow function's parameter list. | |||||
## 6.0.6 (2019-01-30) | |||||
### Bug fixes | |||||
The content of class declarations and expressions is now always parsed in strict mode. | |||||
Don't allow `let` or `const` to bind the variable name `let`. | |||||
Treat class declarations as lexical. | |||||
Don't allow a generator function declaration as the sole body of an `if` or `else`. | |||||
Ignore `"use strict"` when after an empty statement. | |||||
Allow string line continuations with special line terminator characters. | |||||
Treat `for` bodies as part of the `for` scope when checking for conflicting bindings. | |||||
Fix bug with parsing `yield` in a `for` loop initializer. | |||||
Implement special cases around scope checking for functions. | |||||
## 6.0.5 (2019-01-02) | |||||
### Bug fixes | |||||
Fix TypeScript type for `Parser.extend` and add `allowAwaitOutsideFunction` to options type. | |||||
Don't treat `let` as a keyword when the next token is `{` on the next line. | |||||
Fix bug that broke checking for parentheses around an object pattern in a destructuring assignment when `preserveParens` was on. | |||||
## 6.0.4 (2018-11-05) | |||||
### Bug fixes | |||||
Further improvements to tokenizing regular expressions in corner cases. | |||||
## 6.0.3 (2018-11-04) | |||||
### Bug fixes | |||||
Fix bug in tokenizing an expression-less return followed by a function followed by a regular expression. | |||||
Remove stray symlink in the package tarball. | |||||
## 6.0.2 (2018-09-26) | |||||
### Bug fixes | |||||
Fix bug where default expressions could fail to parse inside an object destructuring assignment expression. | |||||
## 6.0.1 (2018-09-14) | |||||
### Bug fixes | |||||
Fix wrong value in `version` export. | |||||
## 6.0.0 (2018-09-14) | |||||
### Bug fixes | |||||
Better handle variable-redefinition checks for catch bindings and functions directly under if statements. | |||||
Forbid `new.target` in top-level arrow functions. | |||||
Fix issue with parsing a regexp after `yield` in some contexts. | |||||
### New features | |||||
The package now comes with TypeScript definitions. | |||||
### Breaking changes | |||||
The default value of the `ecmaVersion` option is now 9 (2018). | |||||
Plugins work differently, and will have to be rewritten to work with this version. | |||||
The loose parser and walker have been moved into separate packages (`acorn-loose` and `acorn-walk`). | |||||
## 5.7.3 (2018-09-10) | |||||
### Bug fixes | |||||
Fix failure to tokenize regexps after expressions like `x.of`. | |||||
Better error message for unterminated template literals. | |||||
## 5.7.2 (2018-08-24) | |||||
### Bug fixes | |||||
Properly handle `allowAwaitOutsideFunction` in for statements. | |||||
Treat function declarations at the top level of modules like let bindings. | |||||
Don't allow async function declarations as the only statement under a label. | |||||
## 5.7.0 (2018-06-15) | |||||
### New features | |||||
Upgraded to Unicode 11. | |||||
## 5.6.0 (2018-05-31) | |||||
### New features | |||||
Allow U+2028 and U+2029 in string when ECMAVersion >= 10. | |||||
Allow binding-less catch statements when ECMAVersion >= 10. | |||||
Add `allowAwaitOutsideFunction` option for parsing top-level `await`. | |||||
## 5.5.3 (2018-03-08) | |||||
### Bug fixes | |||||
A _second_ republish of the code in 5.5.1, this time with yarn, to hopefully get valid timestamps. | |||||
## 5.5.2 (2018-03-08) | |||||
### Bug fixes | |||||
A republish of the code in 5.5.1 in an attempt to solve an issue with the file timestamps in the npm package being 0. | |||||
## 5.5.1 (2018-03-06) | |||||
### Bug fixes | |||||
Fix misleading error message for octal escapes in template strings. | |||||
## 5.5.0 (2018-02-27) | |||||
### New features | |||||
The identifier character categorization is now based on Unicode version 10. | |||||
Acorn will now validate the content of regular expressions, including new ES9 features. | |||||
## 5.4.0 (2018-02-01) | |||||
### Bug fixes | |||||
Disallow duplicate or escaped flags on regular expressions. | |||||
Disallow octal escapes in strings in strict mode. | |||||
### New features | |||||
Add support for async iteration. | |||||
Add support for object spread and rest. | |||||
## 5.3.0 (2017-12-28) | |||||
### Bug fixes | |||||
Fix parsing of floating point literals with leading zeroes in loose mode. | |||||
Allow duplicate property names in object patterns. | |||||
Don't allow static class methods named `prototype`. | |||||
Disallow async functions directly under `if` or `else`. | |||||
Parse right-hand-side of `for`/`of` as an assignment expression. | |||||
Stricter parsing of `for`/`in`. | |||||
Don't allow unicode escapes in contextual keywords. | |||||
### New features | |||||
Parsing class members was factored into smaller methods to allow plugins to hook into it. | |||||
## 5.2.1 (2017-10-30) | |||||
### Bug fixes | |||||
Fix a token context corruption bug. | |||||
## 5.2.0 (2017-10-30) | |||||
### Bug fixes | |||||
Fix token context tracking for `class` and `function` in property-name position. | |||||
Make sure `%*` isn't parsed as a valid operator. | |||||
Allow shorthand properties `get` and `set` to be followed by default values. | |||||
Disallow `super` when not in callee or object position. | |||||
### New features | |||||
Support [`directive` property](https://github.com/estree/estree/compare/b3de58c9997504d6fba04b72f76e6dd1619ee4eb...1da8e603237144f44710360f8feb7a9977e905e0) on directive expression statements. | |||||
## 5.1.2 (2017-09-04) | |||||
### Bug fixes | |||||
Disable parsing of legacy HTML-style comments in modules. | |||||
Fix parsing of async methods whose names are keywords. | |||||
## 5.1.1 (2017-07-06) | |||||
### Bug fixes | |||||
Fix problem with disambiguating regexp and division after a class. | |||||
## 5.1.0 (2017-07-05) | |||||
### Bug fixes | |||||
Fix tokenizing of regexps in an object-desctructuring `for`/`of` loop and after `yield`. | |||||
Parse zero-prefixed numbers with non-octal digits as decimal. | |||||
Allow object/array patterns in rest parameters. | |||||
Don't error when `yield` is used as a property name. | |||||
Allow `async` as a shorthand object property. | |||||
### New features | |||||
Implement the [template literal revision proposal](https://github.com/tc39/proposal-template-literal-revision) for ES9. | |||||
## 5.0.3 (2017-04-01) | |||||
### Bug fixes | |||||
Fix spurious duplicate variable definition errors for named functions. | |||||
## 5.0.2 (2017-03-30) | |||||
### Bug fixes | |||||
A binary operator after a parenthesized arrow expression is no longer incorrectly treated as an error. | |||||
## 5.0.0 (2017-03-28) | |||||
### Bug fixes | |||||
Raise an error for duplicated lexical bindings. | |||||
Fix spurious error when an assignement expression occurred after a spread expression. | |||||
Accept regular expressions after `of` (in `for`/`of`), `yield` (in a generator), and braced arrow functions. | |||||
Allow labels in front or `var` declarations, even in strict mode. | |||||
### Breaking changes | |||||
Parse declarations following `export default` as declaration nodes, not expressions. This means that class and function declarations nodes can now have `null` as their `id`. | |||||
## 4.0.11 (2017-02-07) | |||||
### Bug fixes | |||||
Allow all forms of member expressions to be parenthesized as lvalue. | |||||
## 4.0.10 (2017-02-07) | |||||
### Bug fixes | |||||
Don't expect semicolons after default-exported functions or classes, even when they are expressions. | |||||
Check for use of `'use strict'` directives in non-simple parameter functions, even when already in strict mode. | |||||
## 4.0.9 (2017-02-06) | |||||
### Bug fixes | |||||
Fix incorrect error raised for parenthesized simple assignment targets, so that `(x) = 1` parses again. | |||||
## 4.0.8 (2017-02-03) | |||||
### Bug fixes | |||||
Solve spurious parenthesized pattern errors by temporarily erring on the side of accepting programs that our delayed errors don't handle correctly yet. | |||||
## 4.0.7 (2017-02-02) | |||||
### Bug fixes | |||||
Accept invalidly rejected code like `(x).y = 2` again. | |||||
Don't raise an error when a function _inside_ strict code has a non-simple parameter list. | |||||
## 4.0.6 (2017-02-02) | |||||
### Bug fixes | |||||
Fix exponential behavior (manifesting itself as a complete hang for even relatively small source files) introduced by the new 'use strict' check. | |||||
## 4.0.5 (2017-02-02) | |||||
### Bug fixes | |||||
Disallow parenthesized pattern expressions. | |||||
Allow keywords as export names. | |||||
Don't allow the `async` keyword to be parenthesized. | |||||
Properly raise an error when a keyword contains a character escape. | |||||
Allow `"use strict"` to appear after other string literal expressions. | |||||
Disallow labeled declarations. | |||||
## 4.0.4 (2016-12-19) | |||||
### Bug fixes | |||||
Fix crash when `export` was followed by a keyword that can't be | |||||
exported. | |||||
## 4.0.3 (2016-08-16) | |||||
### Bug fixes | |||||
Allow regular function declarations inside single-statement `if` branches in loose mode. Forbid them entirely in strict mode. | |||||
Properly parse properties named `async` in ES2017 mode. | |||||
Fix bug where reserved words were broken in ES2017 mode. | |||||
## 4.0.2 (2016-08-11) | |||||
### Bug fixes | |||||
Don't ignore period or 'e' characters after octal numbers. | |||||
Fix broken parsing for call expressions in default parameter values of arrow functions. | |||||
## 4.0.1 (2016-08-08) | |||||
### Bug fixes | |||||
Fix false positives in duplicated export name errors. | |||||
## 4.0.0 (2016-08-07) | |||||
### Breaking changes | |||||
The default `ecmaVersion` option value is now 7. | |||||
A number of internal method signatures changed, so plugins might need to be updated. | |||||
### Bug fixes | |||||
The parser now raises errors on duplicated export names. | |||||
`arguments` and `eval` can now be used in shorthand properties. | |||||
Duplicate parameter names in non-simple argument lists now always produce an error. | |||||
### New features | |||||
The `ecmaVersion` option now also accepts year-style version numbers | |||||
(2015, etc). | |||||
Support for `async`/`await` syntax when `ecmaVersion` is >= 8. | |||||
Support for trailing commas in call expressions when `ecmaVersion` is >= 8. | |||||
## 3.3.0 (2016-07-25) | |||||
### Bug fixes | |||||
Fix bug in tokenizing of regexp operator after a function declaration. | |||||
Fix parser crash when parsing an array pattern with a hole. | |||||
### New features | |||||
Implement check against complex argument lists in functions that enable strict mode in ES7. | |||||
## 3.2.0 (2016-06-07) | |||||
### Bug fixes | |||||
Improve handling of lack of unicode regexp support in host | |||||
environment. | |||||
Properly reject shorthand properties whose name is a keyword. | |||||
### New features | |||||
Visitors created with `visit.make` now have their base as _prototype_, rather than copying properties into a fresh object. | |||||
## 3.1.0 (2016-04-18) | |||||
### Bug fixes | |||||
Properly tokenize the division operator directly after a function expression. | |||||
Allow trailing comma in destructuring arrays. | |||||
## 3.0.4 (2016-02-25) | |||||
### Fixes | |||||
Allow update expressions as left-hand-side of the ES7 exponential operator. | |||||
## 3.0.2 (2016-02-10) | |||||
### Fixes | |||||
Fix bug that accidentally made `undefined` a reserved word when parsing ES7. | |||||
## 3.0.0 (2016-02-10) | |||||
### Breaking changes | |||||
The default value of the `ecmaVersion` option is now 6 (used to be 5). | |||||
Support for comprehension syntax (which was dropped from the draft spec) has been removed. | |||||
### Fixes | |||||
`let` and `yield` are now “contextual keywords”, meaning you can mostly use them as identifiers in ES5 non-strict code. | |||||
A parenthesized class or function expression after `export default` is now parsed correctly. | |||||
### New features | |||||
When `ecmaVersion` is set to 7, Acorn will parse the exponentiation operator (`**`). | |||||
The identifier character ranges are now based on Unicode 8.0.0. | |||||
Plugins can now override the `raiseRecoverable` method to override the way non-critical errors are handled. | |||||
## 2.7.0 (2016-01-04) | |||||
### Fixes | |||||
Stop allowing rest parameters in setters. | |||||
Disallow `y` rexexp flag in ES5. | |||||
Disallow `\00` and `\000` escapes in strict mode. | |||||
Raise an error when an import name is a reserved word. | |||||
## 2.6.2 (2015-11-10) | |||||
### Fixes | |||||
Don't crash when no options object is passed. | |||||
## 2.6.0 (2015-11-09) | |||||
### Fixes | |||||
Add `await` as a reserved word in module sources. | |||||
Disallow `yield` in a parameter default value for a generator. | |||||
Forbid using a comma after a rest pattern in an array destructuring. | |||||
### New features | |||||
Support parsing stdin in command-line tool. | |||||
## 2.5.0 (2015-10-27) | |||||
### Fixes | |||||
Fix tokenizer support in the command-line tool. | |||||
Stop allowing `new.target` outside of functions. | |||||
Remove legacy `guard` and `guardedHandler` properties from try nodes. | |||||
Stop allowing multiple `__proto__` properties on an object literal in strict mode. | |||||
Don't allow rest parameters to be non-identifier patterns. | |||||
Check for duplicate paramter names in arrow functions. |