Skip to content

Commit 7eae456

Browse files
committed
Custom JSX parser and proper unit tests!
1 parent 26d2a67 commit 7eae456

File tree

3 files changed

+407
-61
lines changed

3 files changed

+407
-61
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"eslintConfig": {
2424
"extends": "developit",
2525
"rules": {
26-
"prefer-const": 0
26+
"prefer-const": 0,
27+
"no-fallthrough": 0
2728
}
2829
},
2930
"jest": {

src/index.mjs

Lines changed: 268 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13,78 +13,286 @@
1313

1414
const CACHE = {};
1515

16-
const TEMPLATE = document.createElement('template');
17-
18-
const reg = /(\$_h\[\d+\])/g;
19-
2016
export default function html(statics) {
21-
const tpl = CACHE[statics] || (CACHE[statics] = build(statics));
17+
const str = statics.join('\0');
18+
const tpl = CACHE[str] || (CACHE[str] = build(str));
2219
// eslint-disable-next-line prefer-rest-params
2320
return tpl(this, arguments);
2421
}
2522

23+
const TAG_START = 60;
24+
const TAG_END = 62;
25+
const EQUALS = 61;
26+
const QUOTE_DOUBLE = 34;
27+
const QUOTE_SINGLE = 39;
28+
const TAB = 9;
29+
const NEWLINE = 10;
30+
const RETURN = 13;
31+
const SPACE = 32;
32+
const SLASH = 47;
33+
34+
const MODE_WHITESPACE = 0;
35+
const MODE_TEXT = 1;
36+
const MODE_TAGNAME = 9;
37+
const MODE_ATTRIBUTE = 13;
38+
const MODE_SKIP = 47;
39+
2640
/** Create a template function given strings from a tagged template. */
27-
function build(statics) {
28-
let str = statics[0], i = 1;
29-
while (i < statics.length) {
30-
str += '$_h[' + i + ']' + statics[i++];
31-
}
32-
// Template string preprocessing:
33-
// - replace <${Foo}> with <c c@=${Foo}>
34-
// - replace <x /> with <x></x>
35-
// - replace <${Foo}>a<//>b with <c c@=${Foo}>a</c>b
36-
TEMPLATE.innerHTML = str
37-
.replace(/<(?:(\/)\/|(\/?)(\$_h\[\d+\]))/g, '<$1$2c c@=$3')
38-
.replace(/<([\w:-]+)(?:\s[^<>]*?)?(\/?)>/g, (str, name, a) => (
39-
str.replace(/(?:'.*?'|".*?"|([A-Z]))/g, (s, c) => c ? ':::'+c : s) + (a ? '</'+name+'>' : '')
40-
))
41-
.trim();
42-
return Function('h', '$_h', 'return ' + walk((TEMPLATE.content || TEMPLATE).firstChild));
43-
}
41+
function build(input) {
42+
let out = 'return ';
43+
let buffer = '';
44+
let mode = MODE_WHITESPACE;
45+
let fieldIndex = 1;
46+
let field = '';
47+
let hasChildren = 0;
48+
let propCount = 0;
49+
let spreads = 0;
50+
let quote = 0;
51+
let spread, slash, charCode, inTag, propName, propHasValue;
4452

45-
/** Traverse a DOM tree and serialize it to hyperscript function calls */
46-
function walk(n) {
47-
if (n.nodeType != 1) {
48-
if (n.nodeType == 3 && n.data) return field(n.data, ',');
49-
return 'null';
50-
}
51-
let str = '',
52-
nodeName = field(n.localName, str),
53-
sub = '',
54-
start = ',({';
55-
for (let i=0; i<n.attributes.length; i++) {
56-
const name = n.attributes[i].name;
57-
const value = n.attributes[i].value;
58-
if (name=='c@') {
59-
nodeName = value;
53+
function commit() {
54+
if (!inTag) {
55+
if (field || (buffer = buffer.trim())) {
56+
// if (field || buffer) {
57+
if (hasChildren++) out += ',';
58+
out += field || JSON.stringify(buffer);
59+
}
60+
}
61+
else if (mode === MODE_TAGNAME) {
62+
if (hasChildren++) out += ',';
63+
out += 'h(' + (field || JSON.stringify(buffer));
64+
mode = MODE_WHITESPACE;
6065
}
61-
else if (name.substring(0,3)=='...') {
62-
sub = '';
63-
start = ',Object.assign({';
64-
str += '},' + name.substring(3) + ',{';
66+
else if (mode === MODE_ATTRIBUTE || (mode === MODE_WHITESPACE && buffer === '...')) {
67+
// if (!propCount++) {
68+
// propsStart = out.length + 1;
69+
// }
70+
if (mode === MODE_WHITESPACE) {
71+
spread = true;
72+
if (!spreads++) {
73+
if (propCount === 0) out += ',Object.assign({},';
74+
else out = out.replace(/,\(\{(.*?)$/, ',Object.assign({},{$1') + '},';
75+
// out = out.substring(0, propsStart) + out.substring;
76+
}
77+
// out += ',' + field;
78+
out += field + ',{';
79+
propCount++;
80+
}
81+
// out += ',';
82+
else if (propName) {
83+
// out += ',' + propName + ':';
84+
if (!spread) out += ',';
85+
if (propCount === 0) out += '({';
86+
out += propName + ':';
87+
out += field || ((propHasValue || buffer) && JSON.stringify(buffer)) || 'true';
88+
propName = '';
89+
spread = false;
90+
propCount++;
91+
}
92+
propHasValue = false;
6593
}
66-
else {
67-
str += `${sub}"${name.replace(/:::(\w)/g, (s, i) => i.toUpperCase())}":${value ? field(value, '+') : true}`;
68-
sub = ',';
94+
else if (mode === MODE_WHITESPACE) {
95+
// if (buffer === '...') {
96+
// spread = true;
97+
// }
98+
// else {
99+
// spread = false;
100+
mode = MODE_ATTRIBUTE;
101+
// we're in an attribute name
102+
propName = buffer;
103+
buffer = field = '';
104+
commit();
105+
mode = MODE_WHITESPACE;
106+
// }
69107
}
108+
buffer = field = '';
109+
// hasChildren++;
70110
}
71-
str = 'h(' + nodeName + start + str + '})';
72-
let child = n.firstChild;
73-
while (child) {
74-
str += ',' + walk(child);
75-
child = child.nextSibling;
111+
112+
for (let i=0; i<input.length; i++) {
113+
// prevCharCode = charCode;
114+
charCode = input.charCodeAt(i);
115+
field = '';
116+
117+
if (charCode === QUOTE_SINGLE || charCode === QUOTE_DOUBLE) {
118+
if (quote === charCode) {
119+
quote = 0;
120+
// commit();
121+
continue;
122+
}
123+
if (quote === 0) {
124+
quote = charCode;
125+
continue;
126+
}
127+
}
128+
129+
if (charCode === 0) {
130+
// if (mode !== MODE_TAGNAME && mode !== MODE_ATTRIBUTE) commit();
131+
if (!inTag) commit();
132+
field = `$_h[${fieldIndex++}]`;
133+
commit();
134+
continue;
135+
}
136+
137+
if (quote === 0) {
138+
switch (charCode) {
139+
case TAG_START:
140+
if (!inTag) {
141+
// commit buffer
142+
commit();
143+
inTag = true;
144+
propCount = 0;
145+
slash = spread = propHasValue = false;
146+
mode = MODE_TAGNAME;
147+
//if (buffer = buffer.trim()) out += JSON.stringify(buffer);
148+
// if (hasChildren++) out += ',';
149+
// out += 'h(';
150+
// buffer = '';
151+
continue;
152+
}
153+
154+
case TAG_END:
155+
if (inTag) {
156+
commit();
157+
if (mode !== MODE_SKIP) {
158+
// if (prevCharCode === SLASH) {
159+
if (propCount === 0) {
160+
out += ',null';
161+
}
162+
else {
163+
out += '})';
164+
}
165+
}
166+
if (slash) {
167+
// tags.pop();
168+
out += ')';
169+
}
170+
inTag = false;
171+
propCount = 0;
172+
mode = MODE_TEXT;
173+
continue;
174+
}
175+
176+
// case QUOTE_SINGLE:
177+
// case QUOTE_DOUBLE:
178+
// if (quote === charCode) {
179+
// quote = 0;
180+
// }
181+
// if (quote === 0) {
182+
// quote = charCode;
183+
// }
184+
// continue;
185+
186+
case EQUALS:
187+
if (inTag) {
188+
mode = MODE_ATTRIBUTE;
189+
propHasValue = true;
190+
propName = buffer;
191+
buffer = '';
192+
continue;
193+
}
194+
195+
case SLASH:
196+
if (inTag) {
197+
if (!slash) {
198+
slash = true;
199+
// </foo>
200+
// console.log(mode === MODE_TAGNAME, field, buffer.trim());
201+
if (mode === MODE_TAGNAME && !field && !buffer.trim().length) {
202+
buffer = field = '';
203+
mode = MODE_SKIP;
204+
}
205+
}
206+
continue;
207+
}
208+
case TAB:
209+
case NEWLINE:
210+
case RETURN:
211+
case SPACE:
212+
// <a disabled>
213+
if (inTag) {
214+
commit();
215+
continue;
216+
}
217+
// else if (!buffer.length) continue;
218+
// commit = inTag === true;
219+
// continue;
220+
}
221+
}
222+
223+
buffer += input.charAt(i);
224+
225+
// // while ((token = TOKENIZER.exec(statics[i])) || ((field=`$_h[${i}]`), (buffer=''), (lastIndex=0), statics[++i])) {
226+
// // if (char==='\\' || !token) continue;
227+
// if (token[3] != null) break;
228+
// if (!token) {
229+
// if (!inTag) {
230+
// if (buffer) out += JSON.stringify(buffer);
231+
// out += field;
232+
// buffer = '';
233+
// field = null;
234+
// }
235+
// continue;
236+
// }
237+
// buffer += token.input.substring(lastIndex, token.index);
238+
// char = token[0];
239+
// if (!inTag) continue;
240+
// if (prev==='\\') out += prev + char;
241+
// else if (token[1] && !inQuote) inQuote = true;
242+
// else if (inQuote) {
243+
// if (inQuote===token[1]) {
244+
// inQuote = false;
245+
// quotedValue = field || JSON.stringify(buffer);
246+
// }
247+
// }
248+
// else if (char==='<') inTag = true;
249+
// else if (char==='>') inTag = false;
250+
// else if (char==='/' && prev==='<') out += ')';
251+
// else if (char==='=') {
252+
// propName = buffer;
253+
// // out += ',' + JSON.stringify(buffer) + ':';
254+
// }
255+
// else if (token[2]) {
256+
// if (prev==='<') {
257+
// const tag = field || JSON.stringify(buffer);
258+
// tags.push(tag);
259+
// if (buffer) {
260+
// out += JSON.stringify(buffer);
261+
// }
262+
// if (hasChildren) out += ',';
263+
// out += `h(${tag}`;
264+
// buffer = '';
265+
// hasChildren = true;
266+
// // childIndex = 0;
267+
// }
268+
// else {
269+
// out += ',' + JSON.stringify(propName || buffer) + ':' + (quotedValue || 'true');
270+
// buffer = propName = quotedValue = '';
271+
// }
272+
// }
273+
// prev = char;
274+
// field = null;
275+
// lastIndex = TOKENIZER.lastIndex;
76276
}
77-
return str + ')';
277+
commit();
278+
return Function('h', '$_h', out);
279+
// try {
280+
// return Function('h', '$_h', out);
281+
// }
282+
// catch (e) {
283+
// throw `input: ${out}\n${e}`;
284+
// }
78285
}
79286

287+
80288
/** Serialize a field to a String or reference for use in generated code. */
81-
function field(value, sep) {
82-
const matches = value.match(reg);
83-
let strValue = JSON.stringify(value);
84-
if (matches != null) {
85-
if (matches[0] === value) return value;
86-
strValue = strValue.replace(reg, `"${sep}$1${sep}"`).replace(/"[+,]"/g, '');
87-
if (sep == ',') strValue = `[${strValue}]`;
88-
}
89-
return strValue;
90-
}
289+
// function field(value, sep) {
290+
// const matches = value.match(reg);
291+
// let strValue = JSON.stringify(value);
292+
// if (matches != null) {
293+
// if (matches[0] === value) return value;
294+
// strValue = strValue.replace(reg, `"${sep}$1${sep}"`).replace(/"[+,]"/g, '');
295+
// if (sep == ',') strValue = `[${strValue}]`;
296+
// }
297+
// return strValue;
298+
// }

0 commit comments

Comments
 (0)