Skip to content

Commit bfb1644

Browse files
committed
Implement dynamic + static value mixing for babel-plugin-htm
1 parent fdd889c commit bfb1644

File tree

4 files changed

+328
-240
lines changed

4 files changed

+328
-240
lines changed

packages/babel-plugin-htm/index.mjs

Lines changed: 36 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import htm from 'htm';
1+
import { build, treeify } from '../../src/build.mjs';
22

33
/**
44
* @param {Babel} babel
@@ -14,8 +14,6 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
1414
const useBuiltIns = options.useBuiltIns;
1515
const inlineVNodes = options.monomorphic || pragma===false;
1616

17-
const symbol = Symbol();
18-
1917
function dottedIdentifier(keypath) {
2018
const path = keypath.split('.');
2119
let out;
@@ -33,8 +31,10 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
3331
}
3432

3533
function propertyName(key) {
36-
if (key.match(/(^\d|[^a-z0-9_$])/i)) return t.stringLiteral(key);
37-
return t.identifier(key);
34+
if (t.isValidIdentifier(key)) {
35+
return t.identifier(key);
36+
}
37+
return t.stringLiteral(key);
3838
}
3939

4040
function stringValue(str) {
@@ -75,18 +75,10 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
7575
return t.callExpression(pragma, [tag, props].concat(children));
7676
}
7777

78-
function flatten(props, result = []) {
79-
const { [symbol]: head, ...tail } = props;
80-
if (head) head.forEach(obj => {
81-
flatten(obj, result);
82-
});
83-
if (Object.keys(tail).length > 0) {
84-
result.push(tail);
85-
}
86-
return result;
87-
}
88-
8978
function spreadNode(args, state) {
79+
if (args.length === 0) {
80+
return t.nullLiteral();
81+
}
9082
if (args.length > 0 && t.isNode(args[0])) {
9183
args.unshift({});
9284
}
@@ -103,24 +95,40 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
10395
return t.callExpression(helper, args.map(propsNode));
10496
}
10597

98+
function propValueNode(value) {
99+
if (typeof value==='string') {
100+
value = t.stringLiteral(value);
101+
}
102+
else if (typeof value==='boolean') {
103+
value = t.booleanLiteral(value);
104+
}
105+
return value;
106+
}
107+
106108
function propsNode(props) {
107109
return t.isNode(props) ? props : t.objectExpression(
108110
Object.keys(props).map(key => {
109-
let value = props[key];
110-
if (typeof value==='string') {
111-
value = t.stringLiteral(value);
112-
}
113-
else if (typeof value==='boolean') {
114-
value = t.booleanLiteral(value);
115-
}
116-
return t.objectProperty(propertyName(key), value);
111+
const values = props[key];
112+
113+
let node = propValueNode(values[0]);
114+
let isString = t.isStringLiteral(node);
115+
values.slice(1).forEach(value => {
116+
const prop = propValueNode(value);
117+
if (!isString && !t.isStringLiteral(prop)) {
118+
node = t.binaryExpression('+', node, t.stringLiteral(''));
119+
isString = true;
120+
}
121+
node = t.binaryExpression('+', node, prop);
122+
});
123+
124+
return t.objectProperty(propertyName(key), node);
117125
})
118126
);
119127
}
120128

121129
function transform(node, state) {
122130
if (node === undefined) return t.identifier('undefined');
123-
if (node == null) return t.nullLiteral();
131+
if (node === null) return t.nullLiteral();
124132

125133
const { tag, props, children } = node;
126134
function childMapper(child) {
@@ -130,27 +138,10 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
130138
return t.isNode(child) ? child : transform(child, state);
131139
}
132140
const newTag = typeof tag === 'string' ? t.stringLiteral(tag) : tag;
133-
const newProps = props ? spreadNode(flatten(props), state) : t.nullLiteral();
141+
const newProps = spreadNode(props, state);
134142
const newChildren = t.arrayExpression(children.map(childMapper));
135143
return createVNode(newTag, newProps, newChildren);
136144
}
137-
138-
function h(tag, props, ...children) {
139-
return { tag, props, children };
140-
}
141-
142-
const html = htm.bind(h);
143-
144-
function treeify(statics, expr) {
145-
const assign = Object.assign;
146-
try {
147-
Object.assign = function(...objs) { return { [symbol]: objs }; };
148-
return html(statics, ...expr);
149-
}
150-
finally {
151-
Object.assign = assign;
152-
}
153-
}
154145

155146
// The tagged template tag function name we're looking for.
156147
// This is static because it's generally assigned via htm.bind(h),
@@ -165,7 +156,7 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
165156
const statics = path.node.quasi.quasis.map(e => e.value.raw);
166157
const expr = path.node.quasi.expressions;
167158

168-
const tree = treeify(statics, expr);
159+
const tree = treeify(build(statics), expr);
169160
const node = !Array.isArray(tree)
170161
? transform(tree, state)
171162
: t.arrayExpression(tree.map(root => transform(root, state)));
@@ -174,4 +165,4 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
174165
}
175166
}
176167
};
177-
}
168+
}

src/build.mjs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import { MINI } from './constants.mjs';
2+
3+
const MODE_SLASH = 0;
4+
const MODE_TEXT = 1;
5+
const MODE_WHITESPACE = 2;
6+
const MODE_TAGNAME = 3;
7+
const MODE_PROP_SET = 4;
8+
const MODE_PROP_APPEND = 5;
9+
10+
const TAG_SET = 1;
11+
const CHILD_APPEND = 0;
12+
const CHILD_RECURSE = 2;
13+
const PROPS_ASSIGN = 3;
14+
const PROP_SET = MODE_PROP_SET;
15+
const PROP_APPEND = MODE_PROP_APPEND;
16+
17+
// Turn a result of a build(...) call into a tree that is more
18+
// convenient to analyze and transform (e.g. Babel plugins).
19+
// For example:
20+
// treeify(
21+
// build'<div href="1${a}" ...${b}><${x} /></div>`,
22+
// [X, Y, Z]
23+
// )
24+
// returns:
25+
// {
26+
// tag: 'div',
27+
// props: [ { href: ["1", X] }, Y ],
28+
// children: [ { tag: Z, props: [], children: [] } ]
29+
// }
30+
export const treeify = (built, fields) => {
31+
const _treeify = built => {
32+
let tag = '';
33+
let currentProps = null;
34+
const props = [];
35+
const children = [];
36+
37+
for (let i = 1; i < built.length; i++) {
38+
const field = built[i++];
39+
const value = typeof field === 'number' ? fields[field - 1] : field;
40+
41+
if (built[i] === TAG_SET) {
42+
tag = value;
43+
}
44+
else if (built[i] === PROPS_ASSIGN) {
45+
props.push(value);
46+
currentProps = null;
47+
}
48+
else if (built[i] === PROP_SET) {
49+
if (!currentProps) {
50+
currentProps = Object.create(null);
51+
props.push(currentProps);
52+
}
53+
currentProps[built[++i]] = [value];
54+
}
55+
else if (built[i] === PROP_APPEND) {
56+
currentProps[built[++i]].push(value);
57+
}
58+
else if (built[i] === CHILD_RECURSE) {
59+
children.push(_treeify(value));
60+
}
61+
else if (built[i] === CHILD_APPEND) {
62+
children.push(value);
63+
}
64+
}
65+
66+
return { tag, props, children };
67+
};
68+
const { children } = _treeify(built);
69+
return children.length > 1 ? children : children[0];
70+
};
71+
72+
73+
export const evaluate = (h, built, fields, args) => {
74+
for (let i = 1; i < built.length; i++) {
75+
const field = built[i++];
76+
const value = typeof field === 'number' ? fields[field] : field;
77+
78+
if (built[i] === TAG_SET) {
79+
args[0] = value;
80+
}
81+
else if (built[i] === PROP_APPEND) {
82+
args[1][built[++i]] += (value + '');
83+
}
84+
else if (built[i] === PROP_SET) {
85+
(args[1] = args[1] || {})[built[++i]] = value;
86+
}
87+
else if (built[i] === PROPS_ASSIGN) {
88+
args[1] = Object.assign(args[1] || {}, value);
89+
}
90+
else if (built[i]) {
91+
// code === CHILD_RECURSE
92+
args.push(h.apply(null, evaluate(h, value, fields, ['', null])));
93+
}
94+
else {
95+
// code === CHILD_APPEND
96+
args.push(value);
97+
}
98+
}
99+
100+
return args;
101+
};
102+
103+
export const build = function(statics) {
104+
const fields = arguments;
105+
const h = this;
106+
107+
let mode = MODE_TEXT;
108+
let buffer = '';
109+
let quote = '';
110+
let current = [0];
111+
let char, propName;
112+
113+
const commit = field => {
114+
if (mode === MODE_TEXT && (field || (buffer = buffer.replace(/^\s*\n\s*|\s*\n\s*$/g,'')))) {
115+
if (MINI) {
116+
current.push(field ? fields[field] : buffer);
117+
}
118+
else {
119+
current.push(field || buffer, CHILD_APPEND);
120+
}
121+
}
122+
else if (mode === MODE_TAGNAME && (field || buffer)) {
123+
if (MINI) {
124+
current[1] = field ? fields[field] : buffer;
125+
}
126+
else {
127+
current.push(field || buffer, TAG_SET);
128+
}
129+
mode = MODE_WHITESPACE;
130+
}
131+
else if (mode === MODE_WHITESPACE && buffer === '...' && field) {
132+
if (MINI) {
133+
current[2] = Object.assign(current[2] || {}, fields[field]);
134+
}
135+
else {
136+
current.push(field, PROPS_ASSIGN);
137+
}
138+
}
139+
else if (mode === MODE_WHITESPACE && buffer && !field) {
140+
if (MINI) {
141+
(current[2] = current[2] || {})[buffer] = true;
142+
}
143+
else {
144+
current.push(true, PROP_SET, buffer);
145+
}
146+
}
147+
else if (MINI && mode === MODE_PROP_SET) {
148+
(current[2] = current[2] || {})[propName] = field ? buffer ? (buffer + fields[field]) : fields[field] : buffer;
149+
mode = MODE_PROP_APPEND;
150+
}
151+
else if (MINI && mode == MODE_PROP_APPEND) {
152+
if (buffer || field) {
153+
current[2][propName] += field ? buffer + fields[field] : buffer;
154+
}
155+
}
156+
else if (!MINI && mode >= MODE_PROP_SET) {
157+
if (buffer) {
158+
current.push(buffer, mode, propName);
159+
mode = MODE_PROP_APPEND;
160+
}
161+
if (field) {
162+
current.push(field, mode, propName);
163+
}
164+
mode = MODE_PROP_APPEND;
165+
}
166+
buffer = '';
167+
};
168+
169+
for (let i=0; i<statics.length; i++) {
170+
if (i) {
171+
if (mode === MODE_TEXT) {
172+
commit();
173+
}
174+
commit(i);
175+
}
176+
177+
for (let j=0; j<statics[i].length; j++) {
178+
char = statics[i][j];
179+
180+
if (mode === MODE_TEXT) {
181+
if (char === '<') {
182+
// commit buffer
183+
commit();
184+
if (MINI) {
185+
current = [current, '', null];
186+
}
187+
else {
188+
current = [current];
189+
}
190+
mode = MODE_TAGNAME;
191+
}
192+
else {
193+
buffer += char;
194+
}
195+
}
196+
else if (quote) {
197+
if (char === quote) {
198+
quote = '';
199+
}
200+
else {
201+
buffer += char;
202+
}
203+
}
204+
else if (char === '"' || char === "'") {
205+
quote = char;
206+
}
207+
else if (char === '>') {
208+
commit();
209+
mode = MODE_TEXT;
210+
}
211+
else if (!mode) {
212+
// Ignore everything until the tag ends
213+
}
214+
else if (char === '=') {
215+
mode = MODE_PROP_SET;
216+
propName = buffer;
217+
buffer = '';
218+
if (!MINI) {
219+
current.push(buffer, mode, propName);
220+
}
221+
}
222+
else if (char === '/') {
223+
commit();
224+
if (mode === MODE_TAGNAME) {
225+
current = current[0];
226+
}
227+
mode = current;
228+
if (MINI) {
229+
(current = current[0]).push(h.apply(null, mode.slice(1)));
230+
}
231+
else {
232+
(current = current[0]).push(mode, CHILD_RECURSE);
233+
}
234+
mode = MODE_SLASH;
235+
}
236+
else if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
237+
// <a disabled>
238+
commit();
239+
mode = MODE_WHITESPACE;
240+
}
241+
else {
242+
buffer += char;
243+
}
244+
}
245+
}
246+
commit();
247+
248+
if (MINI) {
249+
return current.length > 2 ? current.slice(1) : current[1];
250+
}
251+
return current;
252+
};

0 commit comments

Comments
 (0)