Skip to content

Commit e791a4c

Browse files
committed
Add JSDoc based types
1 parent 53246c3 commit e791a4c

File tree

5 files changed

+158
-43
lines changed

5 files changed

+158
-43
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
2+
*.d.ts
23
*.log
34
coverage/
45
node_modules/

index.js

Lines changed: 118 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
/**
2+
* @typedef {import('unist').Node} Node
3+
* @typedef {import('hast').Root} HastRoot
4+
* @typedef {import('hast').DocType} HastDoctype
5+
* @typedef {import('hast').Element} HastElement
6+
* @typedef {import('hast').Comment} HastComment
7+
* @typedef {import('hast').Text} HastText
8+
* @typedef {import('hast').Properties[string]} HastPropertyValue
9+
* @typedef {import('property-information').html['property'][string]} Info
10+
* @typedef {import('xast').Root} BaseXastRoot
11+
* @typedef {import('xast').Element} XastElement
12+
* @typedef {import('xast').Text} XastText
13+
* @typedef {import('xast').Comment} XastComment
14+
* @typedef {import('xast').Doctype} XastDoctype
15+
* @typedef {import('xast').Attributes} XastAttributes
16+
* @typedef {import('xast').RootChildMap} RootChildMap
17+
* @typedef {HastRoot|HastDoctype|HastElement|HastComment|HastText} HastNode
18+
* @typedef {BaseXastRoot & {children: Array<XastElement|XastText|XastComment|XastDoctype>}} XastRoot
19+
* @typedef {XastRoot|XastElement|XastText|XastComment|XastDoctype} XastNode
20+
*
21+
* @typedef {'html'|'svg'} Space
22+
* @typedef Options
23+
* @property {Space} [space]
24+
*
25+
* @typedef {html|svg} Schema
26+
* @typedef {webNamespaces[Space]} Namespace
27+
*
28+
* @typedef Context
29+
* @property {Schema} schema
30+
* @property {Namespace} ns
31+
*/
32+
133
import {stringify as commas} from 'comma-separated-tokens'
234
import {stringify as spaces} from 'space-separated-tokens'
335
import {html, svg, find} from 'property-information'
@@ -13,52 +45,86 @@ var one = zwitch('type', {
1345
unknown
1446
})
1547

48+
/**
49+
* @param {unknown} value
50+
*/
1651
function invalid(value) {
1752
throw new Error('Expected node, not `' + value + '`')
1853
}
1954

55+
/**
56+
* @param {Node} value
57+
*/
2058
function unknown(value) {
2159
throw new Error('Cannot transform node of type `' + value.type + '`')
2260
}
2361

62+
/**
63+
* @param {HastNode} tree
64+
* @param {Space|Options} [options]
65+
*/
2466
export function toXast(tree, options) {
2567
var space = typeof options === 'string' ? options : (options || {}).space
68+
// @ts-ignore types are wrong.
2669
return one(tree, {schema: space === 'svg' ? svg : html, ns: null})
2770
}
2871

72+
/**
73+
* @param {HastRoot} node
74+
* @param {Context} config
75+
* @returns {XastRoot}
76+
*/
2977
function root(node, config) {
30-
return patch(node, {type: 'root'}, config)
78+
return patch(node, {type: 'root', children: all(node, config)})
3179
}
3280

33-
function text(node, config) {
34-
return patch(node, {type: 'text', value: node.value || ''}, config)
81+
/**
82+
* @param {HastText} node
83+
* @returns {XastText}
84+
*/
85+
function text(node) {
86+
return patch(node, {type: 'text', value: node.value || ''})
3587
}
3688

37-
function comment(node, config) {
38-
return patch(node, {type: 'comment', value: node.value || ''}, config)
89+
/**
90+
* @param {HastComment} node
91+
* @returns {XastComment}
92+
*/
93+
function comment(node) {
94+
return patch(node, {type: 'comment', value: node.value || ''})
3995
}
4096

41-
function doctype(node, config) {
42-
return patch(
43-
node,
44-
{
45-
type: 'doctype',
46-
name: node.name || '',
47-
public: node.public,
48-
system: node.system
49-
},
50-
config
51-
)
97+
/**
98+
* @param {HastDoctype} node
99+
* @returns {XastDoctype}
100+
*/
101+
function doctype(node) {
102+
return patch(node, {
103+
type: 'doctype',
104+
name: 'html',
105+
public: undefined,
106+
system: undefined
107+
})
52108
}
53109

110+
/**
111+
* @param {HastElement} node
112+
* @param {Context} parentConfig
113+
* @returns {XastElement}
114+
*/
54115
// eslint-disable-next-line complexity
55116
function element(node, parentConfig) {
56117
var props = node.properties || {}
57118
var schema = parentConfig.schema
119+
/** @type {XastAttributes} */
58120
var attributes = {}
121+
/** @type {Context} */
59122
var config
123+
/** @type {HastPropertyValue} */
60124
var value
125+
/** @type {string} */
61126
var key
127+
/** @type {Info} */
62128
var info
63129

64130
if (props.xmlns === webNamespaces.html) {
@@ -81,6 +147,7 @@ function element(node, parentConfig) {
81147
}
82148

83149
for (key in props) {
150+
/* c8 ignore next 3 */
84151
if (!own.call(props, key)) {
85152
continue
86153
}
@@ -105,7 +172,7 @@ function element(node, parentConfig) {
105172
}
106173
// Accept `array`.
107174
// Most props are space-separated.
108-
else if (typeof value === 'object' && 'length' in value) {
175+
else if (Array.isArray(value)) {
109176
value = info.commaSeparated ? commas(value) : spaces(value)
110177
}
111178
// Cast everything else to string.
@@ -116,33 +183,50 @@ function element(node, parentConfig) {
116183
attributes[info.attribute] = value
117184
}
118185

119-
return patch(node, {type: 'element', name: node.tagName, attributes}, config)
186+
// @ts-ignore Assume no `doctypes` in `element.`
187+
return patch(node, {
188+
type: 'element',
189+
name: node.tagName,
190+
attributes,
191+
children: all(node, config)
192+
})
120193
}
121194

122-
function patch(origin, node, config) {
123-
var index
195+
/**
196+
* @param {HastRoot|HastElement} origin
197+
* @param {Context} config
198+
* @returns {Array.<XastElement|XastText|XastComment|XastDoctype>}
199+
*/
200+
function all(origin, config) {
201+
/** @type {Array.<XastElement|XastText|XastComment|XastDoctype>} */
202+
var result = []
203+
var index = -1
124204

125205
if (
126206
config.schema === html &&
127207
origin.type === 'element' &&
128-
origin.tagName === 'template'
208+
origin.tagName === 'template' &&
209+
origin.content
129210
) {
130-
node.children = root(origin.content, config).children
131-
} else if (
132-
origin.children &&
133-
(origin.type === 'element' || origin.type === 'root')
134-
) {
135-
node.children = []
136-
index = -1
137-
138-
while (++index < origin.children.length) {
139-
node.children[index] = one(origin.children[index], config)
140-
}
211+
return root(origin.content, config).children
141212
}
142213

143-
if (origin.position) {
144-
node.position = position(origin)
214+
while (++index < origin.children.length) {
215+
// @ts-ignore `zwitch` types are wrong.
216+
result[index] = one(origin.children[index], config)
145217
}
146218

219+
return result
220+
}
221+
222+
/**
223+
* @template {XastNode} X
224+
* @param {HastNode} origin
225+
* @param {X} node
226+
* @returns {X}
227+
*/
228+
function patch(origin, node) {
229+
if (origin.position) node.position = position(origin)
230+
147231
return node
148232
}

package.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@
2828
"sideEffects": false,
2929
"type": "module",
3030
"main": "index.js",
31+
"types": "index.d.ts",
3132
"files": [
33+
"index.d.ts",
3234
"index.js"
3335
],
3436
"dependencies": {
37+
"@types/hast": "^2.0.0",
38+
"@types/unist": "^2.0.0",
39+
"@types/xast": "^1.0.0",
3540
"comma-separated-tokens": "^2.0.0",
3641
"property-information": "^6.0.0",
3742
"space-separated-tokens": "^2.0.0",
@@ -40,21 +45,27 @@
4045
"zwitch": "^2.0.0"
4146
},
4247
"devDependencies": {
48+
"@types/tape": "^4.0.0",
4349
"c8": "^7.0.0",
4450
"hastscript": "^6.0.0",
4551
"prettier": "^2.0.0",
4652
"remark-cli": "^9.0.0",
4753
"remark-preset-wooorm": "^8.0.0",
54+
"rimraf": "^3.0.0",
4855
"tape": "^5.0.0",
56+
"type-coverage": "^2.0.0",
57+
"typescript": "^4.0.0",
4958
"unist-builder": "^3.0.0",
5059
"xastscript": "^2.0.0",
5160
"xo": "^0.39.0"
5261
},
5362
"scripts": {
63+
"prepack": "npm run build && npm run format",
64+
"build": "rimraf \"*.d.ts\" && tsc && type-coverage",
5465
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
5566
"test-api": "node test.js",
5667
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js",
57-
"test": "npm run format && npm run test-coverage"
68+
"test": "npm run build && npm run format && npm run test-coverage"
5869
},
5970
"prettier": {
6071
"tabWidth": 2,
@@ -75,5 +86,10 @@
7586
"plugins": [
7687
"preset-wooorm"
7788
]
89+
},
90+
"typeCoverage": {
91+
"atLeast": 100,
92+
"detail": true,
93+
"strict": true
7894
}
7995
}

test.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ test('toXast', function (t) {
1212

1313
t.throws(
1414
function () {
15+
// @ts-ignore runtime.
1516
toXast()
1617
},
1718
/Error: Expected node, not `undefined`/,
@@ -20,6 +21,7 @@ test('toXast', function (t) {
2021

2122
t.throws(
2223
function () {
24+
// @ts-ignore well-known.
2325
toXast({type: 'raw', value: '<script>alert(1)</script>'})
2426
},
2527
/Error: Cannot transform node of type `raw`/,
@@ -85,6 +87,7 @@ test('toXast', function (t) {
8587
)
8688

8789
t.deepEqual(
90+
// @ts-ignore runtime.
8891
toXast(u('text')),
8992
u('text', ''),
9093
'should support a void text node'
@@ -101,6 +104,7 @@ test('toXast', function (t) {
101104
)
102105

103106
t.deepEqual(
107+
// @ts-ignore runtime.
104108
toXast(u('comment')),
105109
u('comment', ''),
106110
'should support a void comment node'
@@ -111,15 +115,10 @@ test('toXast', function (t) {
111115

112116
t.test('doctype', function (t) {
113117
t.deepEqual(
114-
toXast(u('doctype', {name: 'a'})),
115-
u('doctype', {name: 'a', public: undefined, system: undefined}),
116-
'should support a doctype node'
117-
)
118-
119-
t.deepEqual(
118+
// @ts-ignore hast@next.
120119
toXast(u('doctype')),
121-
u('doctype', {name: '', public: undefined, system: undefined}),
122-
'should support a doctype without a name'
120+
u('doctype', {name: 'html', public: undefined, system: undefined}),
121+
'should support a doctype node'
123122
)
124123

125124
t.end()

tsconfig.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"include": ["*.js"],
3+
"compilerOptions": {
4+
"target": "ES2020",
5+
"lib": ["ES2020"],
6+
"module": "ES2020",
7+
"moduleResolution": "node",
8+
"allowJs": true,
9+
"checkJs": true,
10+
"declaration": true,
11+
"emitDeclarationOnly": true,
12+
"allowSyntheticDefaultImports": true,
13+
"skipLibCheck": true
14+
}
15+
}

0 commit comments

Comments
 (0)