Skip to content

Commit 444c21e

Browse files
committed
Merge branch 'custom-jsx-parser' of github.com:developit/htm into custom-jsx-parser
2 parents 7f84081 + 6cb4a7a commit 444c21e

File tree

2 files changed

+118
-95
lines changed

2 files changed

+118
-95
lines changed

src/index.mjs

Lines changed: 92 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
const CACHE = {};
1515

1616
export default function html(statics) {
17-
const str = statics.join('\0');
18-
const tpl = CACHE[str] || (CACHE[str] = build(str));
17+
const key = statics.reduce((key, s) => key + s.length + '$' + s, '');
18+
const tpl = CACHE[key] || (CACHE[key] = build(statics));
1919
// eslint-disable-next-line prefer-rest-params
2020
return tpl(this, arguments);
2121
}
@@ -38,15 +38,13 @@ const MODE_ATTRIBUTE = 13;
3838
const MODE_SKIP = 47;
3939

4040
/** Create a template function given strings from a tagged template. */
41-
function build(input) {
41+
function build(statics) {
4242
let out = 'return ';
4343
let buffer = '';
4444
let mode = MODE_WHITESPACE;
45-
let fieldIndex = 1;
4645
let field = '';
4746
let hasChildren = 0;
48-
let propCount = 0;
49-
let spreads = 0;
47+
let props = '';
5048
let quote = 0;
5149
let spread, slash, charCode, inTag, propName, propHasValue;
5250

@@ -64,22 +62,19 @@ function build(input) {
6462
}
6563
else if (mode === MODE_ATTRIBUTE || (mode === MODE_WHITESPACE && buffer === '...')) {
6664
if (mode === MODE_WHITESPACE) {
67-
spread = true;
68-
if (!spreads++) {
69-
if (propCount === 0) out += ',Object.assign({},';
70-
else out = out.replace(/,\(\{(.*?)$/, ',Object.assign({},{$1') + '},';
65+
if (!spread) {
66+
spread = true;
67+
if (!props) props = 'Object.assign({},';
68+
else props = 'Object.assign({},' + props + '},';
7169
}
72-
out += field + ',{';
73-
propCount++;
70+
props += field + ',{';
7471
}
7572
else if (propName) {
76-
if (!spread) out += ',';
77-
if (propCount === 0) out += '({';
78-
out += propName + ':';
79-
out += field || ((propHasValue || buffer) && JSON.stringify(buffer)) || 'true';
73+
if (!props) props += '{';
74+
else if (!props.endsWith('{')) props += ',';
75+
props += JSON.stringify(propName) + ':';
76+
props += field || ((propHasValue || buffer) && JSON.stringify(buffer)) || 'true';
8077
propName = '';
81-
spread = false;
82-
propCount++;
8378
}
8479
propHasValue = false;
8580
}
@@ -94,96 +89,98 @@ function build(input) {
9489
buffer = field = '';
9590
}
9691

97-
for (let i=0; i<input.length; i++) {
98-
charCode = input.charCodeAt(i);
99-
field = '';
100-
101-
if (charCode === QUOTE_SINGLE || charCode === QUOTE_DOUBLE) {
102-
if (quote === charCode) {
103-
quote = 0;
104-
continue;
105-
}
106-
if (quote === 0) {
107-
quote = charCode;
108-
continue;
109-
}
110-
}
111-
112-
if (charCode === 0) {
92+
for (let i=0; i<statics.length; i++) {
93+
if (i > 0) {
11394
if (!inTag) commit();
114-
field = `$_h[${fieldIndex++}]`;
95+
field = `$_h[${i}]`;
11596
commit();
116-
continue;
11797
}
98+
99+
const input = statics[i];
100+
for (let j=0; j<input.length; j++) {
101+
charCode = input.charCodeAt(j);
102+
field = '';
118103

119-
if (quote === 0) {
120-
switch (charCode) {
121-
case TAG_START:
122-
if (!inTag) {
123-
// commit buffer
124-
commit();
125-
inTag = true;
126-
propCount = 0;
127-
slash = spread = propHasValue = false;
128-
mode = MODE_TAGNAME;
129-
continue;
130-
}
131-
132-
case TAG_END:
133-
if (inTag) {
134-
commit();
135-
if (mode !== MODE_SKIP) {
136-
if (propCount === 0) {
137-
out += ',null';
104+
if (charCode === QUOTE_SINGLE || charCode === QUOTE_DOUBLE) {
105+
if (quote === charCode) {
106+
quote = 0;
107+
continue;
108+
}
109+
if (quote === 0) {
110+
quote = charCode;
111+
continue;
112+
}
113+
}
114+
115+
if (quote === 0) {
116+
switch (charCode) {
117+
case TAG_START:
118+
if (!inTag) {
119+
// commit buffer
120+
commit();
121+
inTag = true;
122+
props = '';
123+
slash = spread = propHasValue = false;
124+
mode = MODE_TAGNAME;
125+
continue;
126+
}
127+
128+
case TAG_END:
129+
if (inTag) {
130+
commit();
131+
if (mode !== MODE_SKIP) {
132+
if (!props) {
133+
out += ',null';
134+
}
135+
else {
136+
out += ',' + props + '}' + (spread ? ')' : '');
137+
}
138138
}
139-
else {
140-
out += '})';
139+
if (slash) {
140+
out += ')';
141141
}
142+
spread = inTag = false;
143+
props = '';
144+
mode = MODE_TEXT;
145+
continue;
142146
}
143-
if (slash) {
144-
out += ')';
147+
148+
case EQUALS:
149+
if (inTag) {
150+
mode = MODE_ATTRIBUTE;
151+
propHasValue = true;
152+
propName = buffer;
153+
buffer = '';
154+
continue;
145155
}
146-
inTag = false;
147-
propCount = 0;
148-
mode = MODE_TEXT;
149-
continue;
150-
}
151-
152-
case EQUALS:
153-
if (inTag) {
154-
mode = MODE_ATTRIBUTE;
155-
propHasValue = true;
156-
propName = buffer;
157-
buffer = '';
158-
continue;
159-
}
160156

161-
case SLASH:
162-
if (inTag) {
163-
if (!slash) {
164-
slash = true;
165-
// </foo>
166-
if (mode === MODE_TAGNAME && !field && !buffer.trim().length) {
167-
buffer = field = '';
168-
mode = MODE_SKIP;
157+
case SLASH:
158+
if (inTag) {
159+
if (!slash) {
160+
slash = true;
161+
// </foo>
162+
if (mode === MODE_TAGNAME && !field && !buffer.trim().length) {
163+
buffer = field = '';
164+
mode = MODE_SKIP;
165+
}
169166
}
167+
continue;
168+
}
169+
case TAB:
170+
case NEWLINE:
171+
case RETURN:
172+
case SPACE:
173+
// <a disabled>
174+
if (inTag) {
175+
commit();
176+
mode = MODE_WHITESPACE;
177+
continue;
170178
}
171-
continue;
172-
}
173-
case TAB:
174-
case NEWLINE:
175-
case RETURN:
176-
case SPACE:
177-
// <a disabled>
178-
if (inTag) {
179-
commit();
180-
continue;
181-
}
179+
}
182180
}
183-
}
184-
185-
buffer += input.charAt(i);
186181

182+
buffer += input.charAt(j);
183+
}
187184
}
188185
commit();
189186
return Function('h', '$_h', out);

test/index.test.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ describe('htm', () => {
5252
test('single prop with static value', () => {
5353
expect(html`<a href="/hello" />`).toEqual({ tag: 'a', props: { href: '/hello' }, children: [] });
5454
});
55+
56+
test('single prop with static value followed by a single boolean prop', () => {
57+
expect(html`<a href="/hello" b />`).toEqual({ tag: 'a', props: { href: '/hello', b: true }, children: [] });
58+
});
5559

5660
test('two props with static values', () => {
5761
expect(html`<a href="/hello" target="_blank" />`).toEqual({ tag: 'a', props: { href: '/hello', target: '_blank' }, children: [] });
@@ -75,6 +79,9 @@ describe('htm', () => {
7579
expect(html`<a b ...${{ foo: 'bar' }} />`).toEqual({ tag: 'a', props: { b: true, foo: 'bar' }, children: [] });
7680
expect(html`<a b c ...${{ foo: 'bar' }} />`).toEqual({ tag: 'a', props: { b: true, c: true, foo: 'bar' }, children: [] });
7781
expect(html`<a ...${{ foo: 'bar' }} b />`).toEqual({ tag: 'a', props: { b: true, foo: 'bar' }, children: [] });
82+
expect(html`<a b="1" ...${{ foo: 'bar' }} />`).toEqual({ tag: 'a', props: { b: '1', foo: 'bar' }, children: [] });
83+
expect(html`<a x="1"><b y="2" ...${{ c: 'bar' }}/></a>`).toEqual(h('a', { x: '1' }, h('b', { y: '2', c: 'bar' }) ));
84+
expect(html`<a ...${{ c: 'bar' }}><b ...${{ d: 'baz' }}/></a>`).toEqual(h('a', { c: 'bar' }, h('b', { d: 'baz' }) ));
7885
});
7986

8087
test('mixed spread + static props', () => {
@@ -134,4 +141,23 @@ describe('htm', () => {
134141
</a>
135142
`).toEqual(h('a', null, 'before', 'foo', h('b', null), 'bar', 'after'));
136143
});
144+
145+
test('hyphens (-) are allowed in attribute names', () => {
146+
expect(html`<a b-c></a>`).toEqual(h('a', { 'b-c': true }));
147+
});
148+
149+
test('NUL characters are allowed in attribute values', () => {
150+
expect(html`<a b="\0"></a>`).toEqual(h('a', { b: '\0' }));
151+
expect(html`<a b="\0" c=${'foo'}></a>`).toEqual(h('a', { b: '\0', c: 'foo' }));
152+
});
153+
154+
test('NUL characters are allowed in text', () => {
155+
expect(html`<a>\0</a>`).toEqual(h('a', null, '\0'));
156+
expect(html`<a>\0${'foo'}</a>`).toEqual(h('a', null, '\0', 'foo'));
157+
});
158+
159+
test('cache key should be unique', () => {
160+
html`<a b="${'foo'}" />`;
161+
expect(html`<a b="\0" />`).toEqual(h('a', { b: '\0' }));
162+
});
137163
});

0 commit comments

Comments
 (0)