Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
jviide authored Dec 26, 2019
2 parents 8d76204 + c7eb893 commit 93124af
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 64 deletions.
86 changes: 80 additions & 6 deletions packages/babel-plugin-htm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ A Babel plugin that compiles [htm] syntax to hyperscript, React.createElement, o

## Usage

Basic usage:
In your Babel configuration (`.babelrc`, `babel.config.js`, `"babel"` field in package.json, etc), add the plugin:

```js
[
["htm", {
"pragma": "React.createElement"
}]
]
{
"plugins": [
["htm", {
"pragma": "React.createElement"
}]
]
}
```

```js
Expand Down Expand Up @@ -41,6 +43,78 @@ By default, `babel-plugin-htm` will process all Tagged Templates with a tag func
]}
```

### `import=false` _(experimental)_

Auto-import the pragma function, off by default.

#### `false` (default)

Don't auto-import anything.

#### `String`

Import the `pragma` like `import {<pragma>} from '<import>'`.

With Babel config:
```js
"plugins": [
["babel-plugin-htm", {
"tag": "$$html",
"import": "preact"
}]
]
```

```js
import { html as $$html } from 'htm/preact';

export default $$html`<div id="foo">hello ${you}</div>`
```

The above will produce files that look like:

```js
import { h } from 'preact';
import { html as $$html } from 'htm/preact';

export default h("div", { id: "foo" }, "hello ", you)
```

#### `{module: String, export: String}`

Import the `pragma` like `import {<import.export> as <pragma>} from '<import.module>'`.

With Babel config:
```js
"plugins": [
["babel-plugin-htm", {
"pragma": "React.createElement",
"tag": "$$html",
"import": {
// the module to import:
"module": "react",
// a named export to use from that module:
"export": "default"
}
}]
]
```

```js
import { html as $$html } from 'htm/react';

export default $$html`<div id="foo">hello ${you}</div>`
```

The above will produce files that look like:

```js
import React from 'react';
import { html as $$html } from 'htm/react';

export default React.createElement("div", { id: "foo" }, "hello ", you)
```

### `useBuiltIns=false`

`babel-plugin-htm` transforms prop spreads (`<a ...${b}>`) into `Object.assign()` calls. For browser support reasons, Babel's standard `_extends` helper is used by default. To use native `Object.assign` directly, pass `{useBuiltIns:true}`.
Expand Down
46 changes: 40 additions & 6 deletions packages/babel-plugin-htm/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,42 @@ import { build, treeify } from '../../src/build.mjs';
* @param {object} options
* @param {string} [options.pragma=h] JSX/hyperscript pragma.
* @param {string} [options.tag=html] The tagged template "tag" function name to process.
* @param {string | boolean | object} [options.import=false] Import the tag automatically
* @param {boolean} [options.monomorphic=false] Output monomorphic inline objects instead of using String literals.
* @param {boolean} [options.useBuiltIns=false] Use the native Object.assign instead of trying to polyfill it.
* @param {boolean} [options.useNativeSpread=false] Use the native { ...a, ...b } syntax for prop spreads.
* @param {boolean} [options.variableArity=true] If `false`, always passes exactly 3 arguments to the pragma function.
*/
export default function htmBabelPlugin({ types: t }, options = {}) {
const pragma = options.pragma===false ? false : dottedIdentifier(options.pragma || 'h');
const pragmaString = options.pragma===false ? false : options.pragma || 'h';
const pragma = pragmaString===false ? false : dottedIdentifier(pragmaString);
const useBuiltIns = options.useBuiltIns;
const useNativeSpread = options.useNativeSpread;
const inlineVNodes = options.monomorphic || pragma===false;
const importDeclaration = pragmaImport(options.import || false);

function pragmaImport(imp) {
if (pragmaString === false || imp === false) {
return null;
}
const pragmaRoot = t.identifier(pragmaString.split('.')[0]);
const { module, export: export_ } = typeof imp !== 'string' ? imp : {
module: imp,
export: null
};

let specifier;
if (export_ === '*') {
specifier = t.importNamespaceSpecifier(pragmaRoot);
}
else if (export_ === 'default') {
specifier = t.importDefaultSpecifier(pragmaRoot);
}
else {
specifier = t.importSpecifier(pragmaRoot, export_ ? t.identifier(export_) : pragmaRoot);
}
return t.importDeclaration([specifier], t.stringLiteral(module));
}

function dottedIdentifier(keypath) {
const path = keypath.split('.');
Expand Down Expand Up @@ -69,7 +95,7 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
}
return t.stringLiteral(str);
}

function createVNode(tag, props, children) {
// Never pass children=[[]].
if (children.elements.length === 1 && t.isArrayExpression(children.elements[0]) && children.elements[0].elements.length === 0) {
Expand All @@ -94,15 +120,15 @@ export default function htmBabelPlugin({ types: t }, options = {}) {

return t.callExpression(pragma, [tag, props].concat(children));
}

function spreadNode(args, state) {
if (args.length === 0) {
return t.nullLiteral();
}
if (args.length > 0 && t.isNode(args[0])) {
args.unshift({});
}

// 'Object.assign(x)', can be collapsed to 'x'.
if (args.length === 1) {
return propsNode(args[0]);
Expand All @@ -124,11 +150,11 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
});
return t.objectExpression(properties);
}

const helper = useBuiltIns ? dottedIdentifier('Object.assign') : state.addHelper('extends');
return t.callExpression(helper, args.map(propsNode));
}

function propsNode(props) {
return t.isNode(props) ? props : t.objectExpression(objectProperties(props));
}
Expand All @@ -152,6 +178,13 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
return {
name: 'htm',
visitor: {
Program: {
exit(path, state) {
if (state.get('hasHtm') && importDeclaration) {
path.unshiftContainer('body', importDeclaration);
}
},
},
TaggedTemplateExpression(path, state) {
const tag = path.node.tag.name;
if (htmlName[0]==='/' ? patternStringToRegExp(htmlName).test(tag) : tag === htmlName) {
Expand All @@ -163,6 +196,7 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
? transform(tree, state)
: t.arrayExpression(tree.map(root => transform(root, state)));
path.replaceWith(node);
state.set('hasHtm', true);
}
}
}
Expand Down
71 changes: 47 additions & 24 deletions src/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const MODE_COMMENT = 4;
const MODE_PROP_SET = 5;
const MODE_PROP_APPEND = 6;

const TAG_SET = 1;
const CHILD_APPEND = 0;
const CHILD_RECURSE = 2;
const PROPS_ASSIGN = 3;
const TAG_SET = 3;
const PROPS_ASSIGN = 4;
const PROP_SET = MODE_PROP_SET;
const PROP_APPEND = MODE_PROP_APPEND;

Expand All @@ -36,30 +36,30 @@ export const treeify = (built, fields) => {
const children = [];

for (let i = 1; i < built.length; i++) {
const field = built[i++];
const value = typeof field === 'number' ? fields[field - 1] : field;
const type = built[i++];
const value = built[i] ? fields[built[i++]-1] : built[++i];

if (built[i] === TAG_SET) {
if (type === TAG_SET) {
tag = value;
}
else if (built[i] === PROPS_ASSIGN) {
else if (type === PROPS_ASSIGN) {
props.push(value);
currentProps = null;
}
else if (built[i] === PROP_SET) {
else if (type === PROP_SET) {
if (!currentProps) {
currentProps = Object.create(null);
props.push(currentProps);
}
currentProps[built[++i]] = [value];
}
else if (built[i] === PROP_APPEND) {
else if (type === PROP_APPEND) {
currentProps[built[++i]].push(value);
}
else if (built[i] === CHILD_RECURSE) {
else if (type === CHILD_RECURSE) {
children.push(_treeify(value));
}
else if (built[i] === CHILD_APPEND) {
else if (type === CHILD_APPEND) {
children.push(value);
}
}
Expand All @@ -70,12 +70,20 @@ export const treeify = (built, fields) => {
return children.length > 1 ? children : children[0];
};


export const evaluate = (h, built, fields, args) => {
let tmp;

// `build()` used the first element of the operation list as
// temporary workspace. Now that `build()` is done we can use
// that space to track whether the current element is "dynamic"
// (i.e. it or any of its descendants depend on dynamic values).
built[0] = 0;

for (let i = 1; i < built.length; i++) {
const field = built[i];
const value = typeof field === 'number' ? fields[field] : field;
const type = built[++i];
const type = built[i++];

// Set `built[0]` to truthy if this element depends on a dynamic value.
const value = built[i] ? fields[built[0] = built[i++]] : built[++i];

if (type === TAG_SET) {
args[0] = value;
Expand All @@ -90,11 +98,26 @@ export const evaluate = (h, built, fields, args) => {
args[1][built[++i]] += (value + '');
}
else if (type) {
// code === CHILD_RECURSE
args.push(h.apply(null, evaluate(h, value, fields, ['', null])));
// type === CHILD_RECURSE
tmp = h.apply(0, evaluate(h, value, fields, ['', null]));
args.push(tmp);

if (value[0]) {
// If the child element is dynamic, then so is the current element.
built[0] = 1;
}
else {
// Rewrite the operation list in-place if the child element is static.
// The currently evaluated piece `CHILD_RECURSE, 0, [...]` becomes
// `CHILD_APPEND, 0, tmp`.
// Essentially the operation list gets optimized for potential future
// re-evaluations.
built[i-2] = CHILD_APPEND;
built[i] = tmp;
}
}
else {
// code === CHILD_APPEND
// type === CHILD_APPEND
args.push(value);
}
}
Expand All @@ -118,15 +141,15 @@ export const build = function(statics) {
current.push(field ? fields[field] : buffer);
}
else {
current.push(field || buffer, CHILD_APPEND);
current.push(CHILD_APPEND, field, buffer);
}
}
else if (mode === MODE_TAGNAME && (field || buffer)) {
if (MINI) {
current[1] = field ? fields[field] : buffer;
}
else {
current.push(field || buffer, TAG_SET);
current.push(TAG_SET, field, buffer);
}
mode = MODE_WHITESPACE;
}
Expand All @@ -135,15 +158,15 @@ export const build = function(statics) {
current[2] = Object.assign(current[2] || {}, fields[field]);
}
else {
current.push(field, PROPS_ASSIGN);
current.push(PROPS_ASSIGN, field, 0);
}
}
else if (mode === MODE_WHITESPACE && buffer && !field) {
if (MINI) {
(current[2] = current[2] || {})[buffer] = true;
}
else {
current.push(true, PROP_SET, buffer);
current.push(PROP_SET, 0, true, buffer);
}
}
else if (mode >= MODE_PROP_SET) {
Expand All @@ -158,11 +181,11 @@ export const build = function(statics) {
}
else {
if (buffer || (!field && mode === MODE_PROP_SET)) {
current.push(buffer, mode, propName);
current.push(mode, 0, buffer, propName);
mode = MODE_PROP_APPEND;
}
if (field) {
current.push(field, mode, propName);
current.push(mode, field, 0, propName);
mode = MODE_PROP_APPEND;
}
}
Expand Down Expand Up @@ -241,7 +264,7 @@ export const build = function(statics) {
(current = current[0]).push(h.apply(null, mode.slice(1)));
}
else {
(current = current[0]).push(mode, CHILD_RECURSE);
(current = current[0]).push(CHILD_RECURSE, 0, mode);
}
mode = MODE_SLASH;
}
Expand Down
Loading

0 comments on commit 93124af

Please sign in to comment.