-
Notifications
You must be signed in to change notification settings - Fork 0
/
markup.js
142 lines (120 loc) · 3.27 KB
/
markup.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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const repeat = (n = 0, c = ' ') => {
return Array.from({ length: n + 1 }).join(c)
}
const randomFunctionName = () => '__' + Math.random().toString(16).slice(-6)
const isTextNode = el => {
const type = typeof el
return type === 'string' || type === 'number'
}
const selfClosingTag = new Set([
'area', 'base', 'br', 'col',
'command', 'embed', 'hr', 'img',
'input', 'keygen', 'link', 'menuitem',
'meta', 'param', 'source', 'track', 'wbr',
])
const loadableTag = new Set([
'body', 'frame', 'iframe', 'img',
'link', 'script', 'style',
])
// usage:
// [tag]
// [tag, {}]
// [tag, '']
// [tag, []]
// [tag, {}, '']
// [tag, {}, []]
function markup(tags, indent = 0, context = {}) {
if(typeof tags === 'function') {
return markup([tags], indent, context)
}
let [tag, attrs, children] = tags
if(typeof attrs === 'object' && !Array.isArray(attrs)) {
// attrs is attrs
} else {
// attrs is children
children = attrs
attrs = {}
}
const childrenArr = !children ? []
: Array.isArray(children) ? children : [children]
if(typeof tag === 'function') {
// TODO keep the function name info as metadata
return markup(tag({
...attrs,
children: childrenArr,
}, context), indent, context)
}
context.fns = context.fns || {}
const fns = {}
const attrPairs = Object.entries(attrs)
const attrsRendered = attrPairs.length === 0 ? '' :
' ' + attrPairs.map(([k, v]) => {
if(typeof v === 'boolean' && v) {
return k
}
if(k === 'onload' && !loadableTag.has(tag)) {
if(!selfClosingTag.has(tag)) {
childrenArr.push(['style', { 'data-onload': v }])
}
return null
}
if(typeof v !== 'function') {
return `${k}="${v}"`
}
const fnString = v.toString()
const existingFnName = context.fns[fnString]
const fnName = existingFnName || randomFunctionName()
fns[fnString] = fnName
if(k === 'data-onload') {
return `onload="${fnName}(event,this.parentNode);` +
'this.parentNode.removeChild(this)"'
}
return `${k}="${fnName}(event,this)"`
}).filter(v => v).join(' ')
let result = ''
const fnPairs = Object.entries(fns)
if(fnPairs.length > 0) {
result += repeat(indent) + '<script>\n'
fnPairs.forEach(([fnBody, fnName]) => {
result += repeat(indent) + `var ${fnName} = ${fnBody};\n`
})
result += repeat(indent) + '</script>\n'
context.fns = {
...context.fns,
...fns,
}
}
result += repeat(indent) + `<${tag}${attrsRendered}>`
// if has only one text content chlidren or no children,
// we don't linebreak
const shouldLinebreak = !(
childrenArr.length === 0
|| childrenArr.length === 1
&& isTextNode(childrenArr[0])
&& !/\n/.test(childrenArr[0] + '')
)
if(shouldLinebreak) {
result += '\n'
}
for(let child of childrenArr) {
if(isTextNode(child)) {
if(shouldLinebreak) {
result += repeat(indent + 1)
}
result += child
} else {
result += markup(child, indent + 1, context)
}
if(shouldLinebreak) {
result += '\n'
}
}
if(shouldLinebreak) {
result += repeat(indent)
}
if(!selfClosingTag.has(tag)) {
result += `</${tag}>`
}
return result
}
export default markup