Skip to content

Commit c190cf0

Browse files
authored
Merge pull request #38 from developit/custom-jsx-parser
Custom JSX Parser
2 parents f255386 + 15eff25 commit c190cf0

File tree

11 files changed

+735
-254
lines changed

11 files changed

+735
-254
lines changed

package.json

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
"version": "1.0.1",
44
"description": "The Tagged Template syntax for Virtual DOM. Only browser-compatible syntax.",
55
"main": "dist/htm.js",
6-
"umd:main": "dist/htm.js",
6+
"umd:main": "dist/htm.umd.js",
77
"module": "dist/htm.mjs",
88
"scripts": {
99
"build": "npm run -s build:main && npm run -s build:preact && npm run -s build:babel",
10-
"build:main": "microbundle src/index.mjs -f es,umd --no-sourcemap --target web",
11-
"build:preact": "microbundle src/integrations/preact/index.mjs -o preact/index.js -f es,umd --external preact,htm --no-sourcemap --target web && cp src/integrations/preact/index.d.ts src/integrations/preact/package.json preact/ && npm run -s build:preact:standalone",
12-
"build:preact:standalone": "microbundle src/integrations/preact/standalone.mjs -o preact/standalone.js -f es,umd --no-sourcemap --target web",
10+
"build:main": "microbundle src/index.mjs -f es,umd --no-sourcemap --target web && microbundle src/cjs.mjs -f iife --no-sourcemap --target web",
11+
"build:preact": "cd src/integrations/preact && npm run build",
1312
"build:babel": "cd packages/babel-plugin-htm && npm run build",
14-
"test": "eslint src/**/*.mjs test && npm run build && jest test",
13+
"test": "eslint src/**/*.mjs test/**/*.mjs && npm run build && jest test",
1514
"release": "npm run build && git checkout --detach && git add -f babel dist preact && git commit -am \"$npm_package_version\" && git tag $npm_package_version && git push --tags && git checkout master"
1615
},
1716
"files": [
@@ -22,18 +21,34 @@
2221
"eslintConfig": {
2322
"extends": "developit",
2423
"rules": {
25-
"prefer-const": 0
24+
"prefer-const": 0,
25+
"no-fallthrough": 0
2626
}
2727
},
2828
"jest": {
2929
"testURL": "http://localhost",
30+
"testMatch": [
31+
"**/__tests__/**/*.?(m)js?(x)",
32+
"**/?(*.)(spec|test).?(m)js?(x)"
33+
],
34+
"transform": {
35+
"\\.m?js$": "babel-jest"
36+
},
37+
"moduleFileExtensions": [
38+
"mjs",
39+
"js"
40+
],
3041
"moduleNameMapper": {
31-
"^htm-babel-plugin$": "<rootDir>/packages/babel-plugin-htm/dist/babel-plugin-htm.js",
32-
"^babel-plugin-htm$": "<rootDir>/packages/babel-plugin-htm/dist/babel-plugin-htm.js",
33-
"^htm$": "<rootDir>/dist/htm.js",
34-
"^htm/preact$": "<rootDir>/preact/index.js"
42+
"^babel-plugin-htm$": "<rootDir>/packages/babel-plugin-htm/index.mjs",
43+
"^htm$": "<rootDir>/src/index.mjs",
44+
"^htm/preact$": "<rootDir>/src/integrations/preact/index.mjs"
3545
}
3646
},
47+
"babel": {
48+
"presets": [
49+
"env"
50+
]
51+
},
3752
"repository": "developit/htm",
3853
"keywords": [
3954
"Hyperscript Tagged Markup",
@@ -49,7 +64,10 @@
4964
"license": "Apache-2.0",
5065
"homepage": "https://github.com/developit/htm",
5166
"devDependencies": {
52-
"@babel/core": "^7.1.2",
67+
"@babel/core": "^7.2.2",
68+
"@babel/preset-env": "^7.1.6",
69+
"babel-jest": "^23.6.0",
70+
"babel-preset-env": "^1.7.0",
5371
"eslint": "^5.2.0",
5472
"eslint-config-developit": "^1.1.1",
5573
"jest": "^23.4.2",

packages/babel-plugin-htm/index.mjs

Lines changed: 61 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
1-
import { JSDOM } from 'jsdom';
2-
const before = global.document;
3-
global.document = new JSDOM().window.document;
4-
const htm = require('htm');
5-
global.document = before;
6-
7-
// htm() uses the HTML parser, which serializes attribute values.
8-
// this is a problem, because composite values here can be made up
9-
// of strings and AST nodes, which serialize to [object Object].
10-
// Since the handoff from AST node handling to htm() is synchronous,
11-
// this global lookup will always reflect the corresponding
12-
// AST-derived values for the current htm() invocation.
13-
let currentExpressions;
1+
import htm from 'htm';
142

153
/**
164
* @param {Babel} babel
175
* @param {object} options
186
* @param {string} [options.pragma=h] JSX/hyperscript pragma.
197
* @param {string} [options.tag=html] The tagged template "tag" function name to process.
208
* @param {boolean} [options.monomorphic=false] Output monomorphic inline objects instead of using String literals.
9+
* @param {boolean} [options.useBuiltIns=false] Use the native Object.assign instead of trying to polyfill it.
10+
* @param {boolean} [options.variableArity=true] If `false`, always passes exactly 3 arguments to the pragma function.
2111
*/
2212
export default function htmBabelPlugin({ types: t }, options = {}) {
2313
const pragma = options.pragma===false ? false : dottedIdentifier(options.pragma || 'h');
24-
14+
const useBuiltIns = options.useBuiltIns;
2515
const inlineVNodes = options.monomorphic || pragma===false;
2616

2717
function dottedIdentifier(keypath) {
@@ -59,6 +49,11 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
5949
}
6050

6151
function createVNode(tag, props, children) {
52+
// Never pass children=[[]].
53+
if (children.elements.length === 1 && t.isArrayExpression(children.elements[0]) && children.elements[0].elements.length === 0) {
54+
children = children.elements[0];
55+
}
56+
6257
if (inlineVNodes) {
6358
return t.objectExpression([
6459
options.monomorphic && t.objectProperty(propertyName('type'), t.numericLiteral(1)),
@@ -68,93 +63,75 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
6863
options.monomorphic && t.objectProperty(propertyName('text'), t.nullLiteral())
6964
].filter(Boolean));
7065
}
71-
72-
return t.callExpression(pragma, [tag, props, children]);
73-
}
7466

75-
let isVNode = t.isCallExpression;
76-
if (inlineVNodes) {
77-
isVNode = node => {
78-
if (!t.isObjectExpression(node)) return false;
79-
return node.properties[0].value.value!==3;
80-
};
81-
}
82-
83-
function childMapper(child, index, children) {
84-
// JSX-style whitespace: (@TODO: remove? doesn't match the browser version)
85-
if (typeof child==='string' && child.trim().length===0 || child==null) {
86-
if (index===0 || index===children.length-1) return null;
87-
}
88-
if (typeof child==='string' && isVNode(children[index-1]) && isVNode(children[index+1])) {
89-
child = child.trim();
90-
}
91-
if (typeof child==='string') {
92-
const matches = child.match(/\$\$\$_h_\[(\d+)\]/);
93-
if (matches) return currentExpressions[matches[1]];
94-
return stringValue(child);
67+
// Passing `{variableArity:false}` always produces `h(tag, props, children)` - where `children` is always an Array.
68+
// Otherwise, the default is `h(tag, props, ...children)`.
69+
if (options.variableArity !== false) {
70+
children = children.elements;
9571
}
96-
return child;
97-
}
9872

99-
function h(tag, props) {
100-
if (typeof tag==='string') {
101-
const matches = tag.match(/\$\$\$_h_\[(\d+)\]/);
102-
if (matches) tag = currentExpressions[matches[1]];
103-
else tag = t.stringLiteral(tag);
73+
return t.callExpression(pragma, [tag, props].concat(children));
74+
}
75+
76+
function spreadNode(args, state) {
77+
// 'Object.assign({}, x)', can be collapsed to 'x'.
78+
if (args.length === 2 && !t.isNode(args[0]) && Object.keys(args[0]).length === 0) {
79+
return propsNode(args[1]);
10480
}
81+
const helper = useBuiltIns ? dottedIdentifier('Object.assign') : state.addHelper('extends');
82+
return t.callExpression(helper, args.map(propsNode));
83+
}
84+
85+
function propsNode(props) {
86+
if (props == null) return t.nullLiteral();
10587

106-
//const propsNode = props==null || Object.keys(props).length===0 ? t.nullLiteral() : t.objectExpression(
107-
const propsNode = t.objectExpression(
88+
return t.isNode(props) ? props : t.objectExpression(
10889
Object.keys(props).map(key => {
10990
let value = props[key];
11091
if (typeof value==='string') {
111-
const tokenizer = /\$\$\$_h_\[(\d+)\]/g;
112-
let token, lhs, root, index=0, lastIndex=0;
113-
const append = expr => {
114-
if (lhs) expr = t.binaryExpression('+', lhs, expr);
115-
root = lhs = expr;
116-
};
117-
while ((token = tokenizer.exec(value))) {
118-
append(t.stringLiteral(value.substring(index, token.index)));
119-
append(currentExpressions[token[1]]);
120-
index = token.index;
121-
lastIndex = tokenizer.lastIndex;
122-
}
123-
if (lastIndex < value.length) {
124-
append(t.stringLiteral(value.substring(lastIndex)));
125-
}
126-
value = root;
92+
value = t.stringLiteral(value);
12793
}
12894
else if (typeof value==='boolean') {
12995
value = t.booleanLiteral(value);
13096
}
13197
return t.objectProperty(propertyName(key), value);
13298
})
13399
);
100+
}
134101

135-
// recursive iteration of possibly nested arrays of children.
136-
let children = [];
137-
if (arguments.length>2) {
138-
const stack = [];
139-
// eslint-disable-next-line prefer-rest-params
140-
for (let i=arguments.length; i-->2; ) stack.push(arguments[i]);
141-
while (stack.length) {
142-
const child = stack.pop();
143-
if (Array.isArray(child)) {
144-
for (let i=child.length; i--; ) stack.push(child[i]);
145-
}
146-
else if (child!=null) {
147-
children.push(child);
148-
}
102+
function transform(node, state) {
103+
if (node === undefined) return t.identifier('undefined');
104+
if (node == null) return t.nullLiteral();
105+
106+
const { tag, props, children } = node;
107+
function childMapper(child) {
108+
if (typeof child==='string') {
109+
return stringValue(child);
149110
}
150-
children = children.map(childMapper).filter(Boolean);
111+
return t.isNode(child) ? child : transform(child, state);
151112
}
152-
children = t.arrayExpression(children);
153-
154-
return createVNode(tag, propsNode, children);
113+
const newTag = typeof tag === 'string' ? t.stringLiteral(tag) : tag;
114+
const newProps = !Array.isArray(props) ? propsNode(props) : spreadNode(props, state);
115+
const newChildren = t.arrayExpression(children.map(childMapper));
116+
return createVNode(newTag, newProps, newChildren);
155117
}
156118

119+
function h(tag, props, ...children) {
120+
return { tag, props, children };
121+
}
122+
157123
const html = htm.bind(h);
124+
125+
function treeify(statics, expr) {
126+
const assign = Object.assign;
127+
try {
128+
Object.assign = function(...objs) { return objs; };
129+
return html(statics, ...expr);
130+
}
131+
finally {
132+
Object.assign = assign;
133+
}
134+
}
158135

159136
// The tagged template tag function name we're looking for.
160137
// This is static because it's generally assigned via htm.bind(h),
@@ -163,13 +140,14 @@ export default function htmBabelPlugin({ types: t }, options = {}) {
163140
return {
164141
name: 'htm',
165142
visitor: {
166-
TaggedTemplateExpression(path) {
143+
TaggedTemplateExpression(path, state) {
167144
const tag = path.node.tag.name;
168145
if (htmlName[0]==='/' ? patternStringToRegExp(htmlName).test(tag) : tag === htmlName) {
169146
const statics = path.node.quasi.quasis.map(e => e.value.raw);
170147
const expr = path.node.quasi.expressions;
171-
currentExpressions = expr;
172-
path.replaceWith(html(statics, ...expr.map((p, i) => `$$$_h_[${i}]`)));
148+
149+
const tree = treeify(statics, expr);
150+
path.replaceWith(transform(tree, state));
173151
}
174152
}
175153
}

packages/babel-plugin-htm/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@
2929
"license": "Apache-2.0",
3030
"homepage": "https://github.com/developit/htm/tree/master/packages/babel-plugin-htm",
3131
"dependencies": {
32-
"htm": "^1.0.0",
33-
"jsdom": "^11.12.0"
32+
"htm": "^1.0.0"
3433
},
3534
"devDependencies": {
3635
"microbundle": "^0.6.0"

src/cjs.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import htm from './index.mjs';
2+
if (typeof module != 'undefined') module.exports = htm;
3+
else self.htm = htm;

0 commit comments

Comments
 (0)