-
Notifications
You must be signed in to change notification settings - Fork 2
/
htm.js
112 lines (95 loc) · 3.39 KB
/
htm.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const FIELD = '\ue000', QUOTES = '\ue001'
export default function htm (statics) {
let h = this, prev = 0, current = [null], field = 0, args, name, value, quotes = [], quote = 0, last, level = 0, pre = false
const evaluate = (str, parts = [], raw) => {
let i = 0
str = (!raw && str === QUOTES ?
quotes[quote++].slice(1,-1) :
str.replace(/\ue001/g, m => quotes[quote++]))
if (!str) return str
str.replace(/\ue000/g, (match, idx) => {
if (idx) parts.push(str.slice(i, idx))
i = idx + 1
return parts.push(arguments[++field])
})
if (i < str.length) parts.push(str.slice(i))
return parts.length > 1 ? parts : parts[0]
}
// close level
const up = () => {
// console.log('-level', current);
[current, last, ...args] = current
current.push(h(last, ...args))
if (pre === level--) pre = false // reset <pre>
}
let str = statics
.join(FIELD)
.replace(/<!--[^]*?-->/g, '')
.replace(/<!\[CDATA\[[^]*\]\]>/g, '')
.replace(/('|")[^\1]*?\1/g, match => (quotes.push(match), QUOTES))
// ...>text<... sequence
str.replace(/(?:^|>)((?:[^<]|<[^\w\ue000\/?!>])*)(?:$|<)/g, (match, text, idx, str) => {
let tag, close
if (idx) {
str.slice(prev, idx)
// <abc/> → <abc />
.replace(/(\S)\/$/, '$1 /')
.split(/\s+/)
.map((part, i) => {
// </tag>, </> .../>
if (part[0] === '/') {
part = part.slice(1)
// ignore duplicate empty closers </input>
if (EMPTY[part]) return
// ignore pairing self-closing tags
close = tag || part || 1
// skip </input>
}
// <tag
else if (!i) {
tag = evaluate(part)
// <p>abc<p>def, <tr><td>x<tr>
if (typeof tag === 'string') { tag = tag.toLowerCase(); while (CLOSE[current[1]+tag]) up() }
current = [current, tag, null]
level++
if (!pre && PRE[tag]) pre = level
// console.log('+level', tag)
if (EMPTY[tag]) close = tag
}
// attr=...
else if (part) {
let props = current[2] || (current[2] = {})
if (part.slice(0, 3) === '...') {
Object.assign(props, arguments[++field])
}
else {
[name, value] = part.split('=');
Array.isArray(value = props[evaluate(name)] = value ? evaluate(value) : true) &&
// if prop value is array - make sure it serializes as string without csv
(value.toString = value.join.bind(value, ''))
}
}
})
}
if (close) {
if (!current[0]) err(`Wrong close tag \`${close}\``)
up()
// if last child is optionally closable - close it too
while (last !== close && CLOSE[last]) up()
}
prev = idx + match.length
// fix text indentation
if (!pre) text = text.replace(/\s*\n\s*/g,'').replace(/\s+/g, ' ')
if (text) evaluate((last = 0, text), current, true)
})
if (current[0] && CLOSE[current[1]]) up()
if (level) err(`Unclosed \`${current[1]}\`.`)
return current.length < 3 ? current[1] : (current.shift(), current)
}
const err = (msg) => { throw SyntaxError(msg) }
// self-closing elements
const EMPTY = htm.empty = {}
// optional closing elements
const CLOSE = htm.close = {}
// preformatted text elements
const PRE = htm.pre = {}