Skip to content

Commit 165db2e

Browse files
committed
Add JSDoc based types
1 parent d9fb67b commit 165db2e

File tree

6 files changed

+182
-41
lines changed

6 files changed

+182
-41
lines changed

index.js

+4
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1+
/**
2+
* @typedef {import('./lib/index.js').Options} Options
3+
*/
4+
15
export {toDom} from './lib/index.js'

lib/index.js

+109-34
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,84 @@
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+
123
import {webNamespaces} from 'web-namespaces'
224
import {find, html, svg} from 'property-information'
325

426
/* eslint-env browser */
527

6-
function transform(node, options) {
28+
/**
29+
* @param {HastNode} node
30+
* @param {Context} [ctx]
31+
*/
32+
function transform(node, ctx) {
733
switch (node.type) {
834
case 'root':
9-
return root(node, options)
35+
return root(node, ctx)
1036
case 'text':
11-
return text(node, options)
37+
return text(node, ctx)
1238
case 'element':
13-
return element(node, options)
39+
return element(node, ctx)
1440
case 'doctype':
15-
return doctype(node, options)
41+
return doctype(node, ctx)
1642
case 'comment':
17-
return comment(node, options)
43+
return comment(node, ctx)
1844
default:
19-
return element(node, options)
45+
return element(node, ctx)
2046
}
2147
}
2248

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
2658
const {children = []} = node
2759
const {length: childrenLength} = children
2860

29-
let namespace = optionsNamespace
61+
let namespace = ctxNamespace
3062
let rootIsDocument = childrenLength === 0
3163

3264
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
3469

35-
if (tagName === 'html') {
3670
// If we have a root HTML node, we don’t need to render as a fragment.
3771
rootIsDocument = true
3872

3973
// 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
4276
}
4377
}
4478
}
4579

4680
// The root node will be a Document, DocumentFragment, or HTMLElement.
81+
/** @type {XMLDocument|DocumentFragment|HTMLHtmlElement} */
4782
let result
4883

4984
if (rootIsDocument) {
@@ -55,14 +90,20 @@ function root(node, options) {
5590
}
5691

5792
return appendAll(result, children, {
58-
...options,
93+
...ctx,
5994
fragment,
6095
namespace,
6196
impliedNamespace: namespace
6297
})
6398
}
6499

65-
// Create a `doctype`.
100+
/**
101+
* Create a `doctype`.
102+
*
103+
* @param {HastDoctype} node
104+
* @param {Context} ctx
105+
* @returns {DocumentType}
106+
*/
66107
function doctype(node, {doc}) {
67108
return doc.implementation.createDocumentType(
68109
node.name || 'html',
@@ -71,21 +112,39 @@ function doctype(node, {doc}) {
71112
)
72113
}
73114

74-
// Create a `text`.
115+
/**
116+
* Create a `text`.
117+
*
118+
* @param {HastText} node
119+
* @param {Context} ctx
120+
* @returns {Text}
121+
*/
75122
function text(node, {doc}) {
76123
return doc.createTextNode(node.value)
77124
}
78125

79-
// Create a `comment`.
126+
/**
127+
* Create a `comment`.
128+
*
129+
* @param {HastComment} node
130+
* @param {Context} ctx
131+
* @returns {Comment}
132+
*/
80133
function comment(node, {doc}) {
81134
return doc.createComment(node.value)
82135
}
83136

84-
// Create an `element`.
137+
/**
138+
* Create an `element`.
139+
*
140+
* @param {HastElement} node
141+
* @param {Context} ctx
142+
* @returns {Element}
143+
*/
85144
// 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
89148
const {
90149
tagName = impliedNamespace === webNamespaces.svg ? 'g' : 'div',
91150
properties = {},
@@ -147,29 +206,45 @@ function element(node, options) {
147206
result.removeAttribute(attribute)
148207
}
149208
} else if (booleanish) {
150-
result.setAttribute(attribute, value)
209+
result.setAttribute(attribute, String(value))
151210
} else if (value === true) {
152211
result.setAttribute(attribute, '')
153212
} else if (value || value === 0 || value === '') {
154-
result.setAttribute(attribute, value)
213+
result.setAttribute(attribute, String(value))
155214
}
156215
}
157216

158-
return appendAll(result, children, {...options, impliedNamespace})
217+
return appendAll(result, children, {...ctx, impliedNamespace})
159218
}
160219

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) {
166233
// eslint-disable-next-line unicorn/prefer-dom-node-append
167-
node.appendChild(transform(children[i], options))
234+
node.appendChild(transform(children[index], ctx))
168235
}
169236

170237
return node
171238
}
172239

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})
175250
}

package.json

+16-1
Original file line numberDiff line numberDiff line change
@@ -27,31 +27,41 @@
2727
"sideEffects": false,
2828
"type": "module",
2929
"main": "index.js",
30+
"types": "index.d.ts",
3031
"files": [
3132
"lib/",
33+
"index.d.ts",
3234
"index.js"
3335
],
3436
"dependencies": {
3537
"property-information": "^6.0.0",
3638
"web-namespaces": "^2.0.0"
3739
},
3840
"devDependencies": {
41+
"@types/jsdom": "^16.0.0",
42+
"@types/tape": "^4.0.0",
43+
"@types/w3c-xmlserializer": "^2.0.0",
3944
"c8": "^7.0.0",
4045
"glob": "^7.0.0",
4146
"hastscript": "^7.0.0",
4247
"jsdom": "^16.5.3",
4348
"prettier": "^2.0.0",
4449
"remark-cli": "^9.0.0",
4550
"remark-preset-wooorm": "^8.0.0",
51+
"rimraf": "^3.0.0",
4652
"tape": "^5.0.0",
53+
"type-coverage": "^2.0.0",
54+
"typescript": "^4.0.0",
4755
"w3c-xmlserializer": "^2.0.0",
4856
"xo": "^0.39.0"
4957
},
5058
"scripts": {
59+
"prepack": "npm run build && npm run format",
60+
"build": "rimraf \"{lib/**,test/**,}*.d.ts\" && tsc && type-coverage",
5161
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
5262
"test-api": "node test/index.js",
5363
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test/index.js",
54-
"test": "npm run format && npm run test-coverage"
64+
"test": "npm run build && npm run format && npm run test-coverage"
5565
},
5666
"prettier": {
5767
"tabWidth": 2,
@@ -68,5 +78,10 @@
6878
"plugins": [
6979
"preset-wooorm"
7080
]
81+
},
82+
"typeCoverage": {
83+
"atLeast": 100,
84+
"detail": true,
85+
"strict": true
7186
}
7287
}

readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Whether a DOM fragment should be returned (default: `false`).
8282

8383
###### `options.document`
8484

85-
Document interface to use (default: `global.document`).
85+
Document interface to use (default: `globalThis.document`).
8686

8787
###### `options.namespace`
8888

0 commit comments

Comments
 (0)