Skip to content

Commit 381843a

Browse files
committed
Add babel plugin (htm/babel) to precompile tagged templates to hyperscript or Virtual DOM objects.
1 parent 91d653e commit 381843a

File tree

3 files changed

+157
-1
lines changed

3 files changed

+157
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/node_modules
22
/package-lock.json
33
/preact
4+
/babel
45
/dist

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
"umd:main": "dist/htm.js",
77
"module": "dist/htm.mjs",
88
"scripts": {
9-
"build": "npm run -s build:main && npm run -s build:preact",
9+
"build": "npm run -s build:main && npm run -s build:preact && npm run -s build:babel",
1010
"build:main": "microbundle src/index.mjs -f es,umd --no-sourcemap --target web",
1111
"build:preact": "microbundle src/integrations/preact.mjs -o preact/index.js -f es,umd --no-sourcemap --target web",
12+
"build:babel": "microbundle src/babel.mjs -o babel/index.js -f es,cjs --target node --no-compress --no-sourcemap",
1213
"test": "eslint src test && jest test"
1314
},
1415
"eslintConfig": {

src/babel.mjs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import htm from './index.mjs';
2+
3+
// htm() uses the HTML parser, which serializes attribute values.
4+
// this is a problem, because composite values here can be made up
5+
// of strings and AST nodes, which serialize to [object Object].
6+
// Since the handoff from AST node handling to htm() is synchronous,
7+
// this global lookup will always reflect the corresponding
8+
// AST-derived values for the current htm() invocation.
9+
let currentExpressions;
10+
11+
/**
12+
* @param {Babel} babel
13+
* @param {object} options
14+
* @param {string} [options.pragma=h] JSX/hyperscript pragma.
15+
* @param {string} [options.tag=html] The tagged template "tag" function name to process.
16+
* @param {boolean} [options.monomorphic=false] Output monomorphic inline objects instead of using String literals.
17+
*/
18+
export default function htmBabelPlugin({ types: t }, options = {}) {
19+
const pragma = options.pragma===false ? false : dottedIdentifier(options.pragma || 'h');
20+
21+
const inlineVNodes = options.monomorphic || pragma===false;
22+
23+
function dottedIdentifier(keypath) {
24+
const path = keypath.split('.');
25+
let out;
26+
for (let i=0; i<path.length; i++) {
27+
const ident = propertyName(path[i]);
28+
out = i===0 ? ident : t.memberExpression(out, ident);
29+
}
30+
return out;
31+
}
32+
33+
function patternStringToRegExp(str) {
34+
const parts = str.split('/').slice(1);
35+
const end = parts.pop() || '';
36+
return new RegExp(parts.join('/'), end);
37+
}
38+
39+
function propertyName(key) {
40+
if (key.match(/(^\d|[^a-z0-9_$])/i)) return t.stringLiteral(key);
41+
return t.identifier(key);
42+
}
43+
44+
function stringValue(str) {
45+
if (options.monomorphic) {
46+
return t.objectExpression([
47+
t.objectProperty(propertyName('type'), t.numericLiteral(3)),
48+
t.objectProperty(propertyName('tag'), t.nullLiteral()),
49+
t.objectProperty(propertyName('props'), t.nullLiteral()),
50+
t.objectProperty(propertyName('children'), t.nullLiteral()),
51+
t.objectProperty(propertyName('text'), t.stringLiteral(str))
52+
]);
53+
}
54+
return t.stringLiteral(str);
55+
}
56+
57+
function createVNode(tag, props, children) {
58+
if (inlineVNodes) {
59+
return t.objectExpression([
60+
options.monomorphic && t.objectProperty(propertyName('type'), t.numericLiteral(1)),
61+
t.objectProperty(propertyName('tag'), tag),
62+
t.objectProperty(propertyName('props'), props),
63+
t.objectProperty(propertyName('children'), children),
64+
options.monomorphic && t.objectProperty(propertyName('text'), t.nullLiteral())
65+
].filter(Boolean));
66+
}
67+
68+
return t.callExpression(pragma, [tag, props, children]);
69+
}
70+
71+
let isVNode = t.isCallExpression;
72+
if (inlineVNodes) {
73+
isVNode = node => {
74+
if (!t.isObjectExpression(node)) return false;
75+
return node.properties[0].value.value!==3;
76+
};
77+
}
78+
79+
function mapChildren(child, index, children) {
80+
// JSX-style whitespace: (@TODO: remove? doesn't match the browser version)
81+
if (typeof child==='string' && child.trim().length===0 || child==null) {
82+
if (index===0 || index===children.length-1) return null;
83+
}
84+
if (typeof child==='string' && isVNode(children[index-1]) && isVNode(children[index+1])) {
85+
child = child.trim();
86+
}
87+
if (typeof child==='string') {
88+
return stringValue(child);
89+
}
90+
return child;
91+
}
92+
93+
function h(tag, props, ...children) {
94+
if (typeof tag==='string') {
95+
const matches = tag.match(/\$\$\$_h_\[(\d+)\]/);
96+
if (matches) tag = currentExpressions[matches[1]];
97+
else tag = t.stringLiteral(tag);
98+
}
99+
//const propsNode = props==null || Object.keys(props).length===0 ? t.nullLiteral() : t.objectExpression(
100+
const propsNode = t.objectExpression(
101+
Object.keys(props).map(key => {
102+
let value = props[key];
103+
if (typeof value==='string') {
104+
const tokenizer = /\$\$\$_h_\[(\d+)\]/g;
105+
let token, lhs, root, index=0, lastIndex=0;
106+
const append = expr => {
107+
if (lhs) expr = t.binaryExpression('+', lhs, expr);
108+
root = lhs = expr;
109+
};
110+
while ((token = tokenizer.exec(value))) {
111+
append(t.stringLiteral(value.substring(index, token.index)));
112+
append(currentExpressions[token[1]]);
113+
index = token.index;
114+
lastIndex = tokenizer.lastIndex;
115+
}
116+
if (lastIndex < value.length) {
117+
append(t.stringLiteral(value.substring(lastIndex)));
118+
}
119+
value = root;
120+
}
121+
else if (typeof value==='boolean') {
122+
value = t.booleanLiteral(value);
123+
}
124+
return t.objectProperty(propertyName(key), value);
125+
})
126+
);
127+
128+
if (Array.isArray(children)) {
129+
children = t.arrayExpression(children.map(mapChildren).filter(Boolean));
130+
}
131+
return createVNode(tag, propsNode, children);
132+
}
133+
134+
const html = htm.bind(h);
135+
136+
// The tagged template tag function name we're looking for.
137+
// This is static because it's generally assigned via htm.bind(h),
138+
// which could be imported from elsewhere, making tracking impossible.
139+
const htmlName = options.tag || 'html';
140+
return {
141+
name: 'htm',
142+
visitor: {
143+
TaggedTemplateExpression(path) {
144+
const tag = path.node.tag.name;
145+
if (htmlName[0]==='/' ? patternStringToRegExp(htmlName).test(tag) : tag === htmlName) {
146+
const statics = path.node.quasi.quasis.map(e => e.value.raw);
147+
const expr = path.node.quasi.expressions;
148+
currentExpressions = expr;
149+
path.replaceWith(html(statics, ...expr.map((p, i) => `$$$_h_[${i}]`)));
150+
}
151+
}
152+
}
153+
};
154+
}

0 commit comments

Comments
 (0)