1
+ /**
2
+ * @typedef {import('hast').Parent} HastParent
3
+ * @typedef {import('hast').Root} HastRoot
4
+ * @typedef {import('hast').DocType} HastDoctype
5
+ * @typedef {import('hast').Element} HastElement
6
+ * @typedef {import('hast').Text} HastText
7
+ * @typedef {import('hast').Comment} HastComment
8
+ * @typedef {HastParent['children'][number]} HastChild
9
+ * @typedef {HastChild|HastRoot} HastNode
10
+ *
11
+ * @typedef Options
12
+ * @property {boolean} [fragment=false] Whether a DOM fragment should be returned
13
+ * @property {Document} [document] Document interface to use (default: `globalThis.document`)
14
+ * @property {string} [namespace] `namespace` to use to create elements
15
+ *
16
+ * @typedef Context
17
+ * @property {Document} doc
18
+ * @property {boolean} [fragment=false]
19
+ * @property {string} [namespace]
20
+ * @property {string} [impliedNamespace]
21
+ */
22
+
1
23
import {webNamespaces} from 'web-namespaces'
2
24
import {find, html, svg} from 'property-information'
3
25
4
26
/* eslint-env browser */
5
27
6
- function transform(node, options) {
28
+ /**
29
+ * @param {HastNode} node
30
+ * @param {Context} [ctx]
31
+ */
32
+ function transform(node, ctx) {
7
33
switch (node.type) {
8
34
case 'root':
9
- return root(node, options )
35
+ return root(node, ctx )
10
36
case 'text':
11
- return text(node, options )
37
+ return text(node, ctx )
12
38
case 'element':
13
- return element(node, options )
39
+ return element(node, ctx )
14
40
case 'doctype':
15
- return doctype(node, options )
41
+ return doctype(node, ctx )
16
42
case 'comment':
17
- return comment(node, options )
43
+ return comment(node, ctx )
18
44
default:
19
- return element(node, options )
45
+ return element(node, ctx )
20
46
}
21
47
}
22
48
23
- // Create a document.
24
- function root(node, options) {
25
- const {doc, fragment, namespace: optionsNamespace} = options
49
+ /**
50
+ * Create a document.
51
+ *
52
+ * @param {HastRoot} node
53
+ * @param {Context} ctx
54
+ * @returns {XMLDocument|DocumentFragment|HTMLHtmlElement}
55
+ */
56
+ function root(node, ctx) {
57
+ const {doc, fragment, namespace: ctxNamespace} = ctx
26
58
const {children = []} = node
27
59
const {length: childrenLength} = children
28
60
29
- let namespace = optionsNamespace
61
+ let namespace = ctxNamespace
30
62
let rootIsDocument = childrenLength === 0
31
63
32
64
for (let i = 0; i < childrenLength; i += 1) {
33
- const {tagName, properties = {}} = children[i]
65
+ const child = children[i]
66
+
67
+ if (child.type === 'element' && child.tagName === 'html') {
68
+ const {properties = {}} = child
34
69
35
- if (tagName === 'html') {
36
70
// If we have a root HTML node, we don’t need to render as a fragment.
37
71
rootIsDocument = true
38
72
39
73
// Take namespace of the first child.
40
- if (typeof optionsNamespace === 'undefined') {
41
- namespace = properties.xmlns || webNamespaces.html
74
+ if (typeof ctxNamespace === 'undefined') {
75
+ namespace = String( properties.xmlns || '') || webNamespaces.html
42
76
}
43
77
}
44
78
}
45
79
46
80
// The root node will be a Document, DocumentFragment, or HTMLElement.
81
+ /** @type {XMLDocument|DocumentFragment|HTMLHtmlElement} */
47
82
let result
48
83
49
84
if (rootIsDocument) {
@@ -55,14 +90,20 @@ function root(node, options) {
55
90
}
56
91
57
92
return appendAll(result, children, {
58
- ...options ,
93
+ ...ctx ,
59
94
fragment,
60
95
namespace,
61
96
impliedNamespace: namespace
62
97
})
63
98
}
64
99
65
- // Create a `doctype`.
100
+ /**
101
+ * Create a `doctype`.
102
+ *
103
+ * @param {HastDoctype} node
104
+ * @param {Context} ctx
105
+ * @returns {DocumentType}
106
+ */
66
107
function doctype(node, {doc}) {
67
108
return doc.implementation.createDocumentType(
68
109
node.name || 'html',
@@ -71,21 +112,39 @@ function doctype(node, {doc}) {
71
112
)
72
113
}
73
114
74
- // Create a `text`.
115
+ /**
116
+ * Create a `text`.
117
+ *
118
+ * @param {HastText} node
119
+ * @param {Context} ctx
120
+ * @returns {Text}
121
+ */
75
122
function text(node, {doc}) {
76
123
return doc.createTextNode(node.value)
77
124
}
78
125
79
- // Create a `comment`.
126
+ /**
127
+ * Create a `comment`.
128
+ *
129
+ * @param {HastComment} node
130
+ * @param {Context} ctx
131
+ * @returns {Comment}
132
+ */
80
133
function comment(node, {doc}) {
81
134
return doc.createComment(node.value)
82
135
}
83
136
84
- // Create an `element`.
137
+ /**
138
+ * Create an `element`.
139
+ *
140
+ * @param {HastElement} node
141
+ * @param {Context} ctx
142
+ * @returns {Element}
143
+ */
85
144
// eslint-disable-next-line complexity
86
- function element(node, options ) {
87
- const {namespace, doc} = options
88
- let impliedNamespace = options .impliedNamespace || namespace
145
+ function element(node, ctx ) {
146
+ const {namespace, doc} = ctx
147
+ let impliedNamespace = ctx .impliedNamespace || namespace
89
148
const {
90
149
tagName = impliedNamespace === webNamespaces.svg ? 'g' : 'div',
91
150
properties = {},
@@ -147,29 +206,45 @@ function element(node, options) {
147
206
result.removeAttribute(attribute)
148
207
}
149
208
} else if (booleanish) {
150
- result.setAttribute(attribute, value)
209
+ result.setAttribute(attribute, String( value) )
151
210
} else if (value === true) {
152
211
result.setAttribute(attribute, '')
153
212
} else if (value || value === 0 || value === '') {
154
- result.setAttribute(attribute, value)
213
+ result.setAttribute(attribute, String( value) )
155
214
}
156
215
}
157
216
158
- return appendAll(result, children, {...options , impliedNamespace})
217
+ return appendAll(result, children, {...ctx , impliedNamespace})
159
218
}
160
219
161
- // Add all children.
162
- function appendAll(node, children, options) {
163
- const childrenLength = children.length
164
-
165
- for (let i = 0; i < childrenLength; i += 1) {
220
+ /**
221
+ * Add all children.
222
+ *
223
+ * @template {Node} N
224
+ * @param {N} node
225
+ * @param {Array.<HastChild>} children
226
+ * @param {Context} ctx
227
+ * @returns {N}
228
+ */
229
+ function appendAll(node, children, ctx) {
230
+ let index = -1
231
+
232
+ while (++index < children.length) {
166
233
// eslint-disable-next-line unicorn/prefer-dom-node-append
167
- node.appendChild(transform(children[i ], options ))
234
+ node.appendChild(transform(children[index ], ctx ))
168
235
}
169
236
170
237
return node
171
238
}
172
239
173
- export function toDom(hast, options = {}) {
174
- return transform(hast, {...options, doc: options.document || document})
240
+ /**
241
+ * Transform a hast tree to a DOM tree
242
+ *
243
+ * @param {HastNode} node
244
+ * @param {Options} [options]
245
+ * @returns {Node}
246
+ */
247
+ export function toDom(node, options = {}) {
248
+ const {document: doc = document, ...rest} = options
249
+ return transform(node, {doc, ...rest})
175
250
}
0 commit comments