Skip to content

Commit 924bdaf

Browse files
authored
Add JSDoc based types
Closes GH-72. Reviewed-by: Christian Murphy <[email protected]>
1 parent fe1c21f commit 924bdaf

16 files changed

+243
-350
lines changed

.gitignore

+1
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

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

lib/contents.js

+94-46
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
1+
/**
2+
* @typedef {import('unist').Node} Node
3+
* @typedef {import('mdast').List} List
4+
* @typedef {import('mdast').ListItem} ListItem
5+
* @typedef {import('mdast').PhrasingContent} PhrasingContent
6+
* @typedef {import('mdast').StaticPhrasingContent} StaticPhrasingContent
7+
* @typedef {import('./search.js').SearchEntry} SearchEntry
8+
*
9+
* @typedef ContentsOptions
10+
* @property {boolean} [tight=false] Whether to compile list-items tightly.
11+
* @property {boolean} [ordered=false] Whether to compile list-items as an ordered list, otherwise they are unordered.
12+
* @property {string} [prefix=null] Add a prefix to links to headings in the table of contents. Useful for example when later going from mdast to hast and sanitizing with `hast-util-sanitize`.
13+
*/
14+
115
import extend from 'extend'
216

3-
// Transform a list of heading objects to a markdown list.
4-
export function contents(map, tight, prefix, ordered) {
17+
/**
18+
* Transform a list of heading objects to a markdown list.
19+
*
20+
* @param {Array.<SearchEntry>} map
21+
* @param {ContentsOptions} settings
22+
*/
23+
export function contents(map, settings) {
24+
const {ordered = false, tight = false, prefix = null} = settings
25+
/** @type {List} */
526
const table = {type: 'list', ordered, spread: false, children: []}
627
let minDepth = Number.POSITIVE_INFINITY
728
let index = -1
@@ -24,103 +45,130 @@ export function contents(map, tight, prefix, ordered) {
2445
index = -1
2546

2647
while (++index < map.length) {
27-
insert(map[index], table, tight, prefix, ordered)
48+
insert(map[index], table, {ordered, tight, prefix})
2849
}
2950

3051
return table
3152
}
3253

33-
// Insert an entry into `parent`.
34-
// eslint-disable-next-line max-params
35-
function insert(entry, parent, tight, prefix, ordered) {
36-
const siblings = parent.children
37-
const tail = siblings[siblings.length - 1]
54+
/**
55+
* Insert an entry into `parent`.
56+
*
57+
* @param {SearchEntry} entry
58+
* @param {List|ListItem} parent
59+
* @param {ContentsOptions} settings
60+
*/
61+
function insert(entry, parent, settings) {
3862
let index = -1
3963

40-
if (entry.depth === 1) {
41-
siblings.push({
42-
type: 'listItem',
43-
spread: false,
44-
children: [
45-
{
46-
type: 'paragraph',
47-
children: [
48-
{
49-
type: 'link',
50-
title: null,
51-
url: '#' + (prefix || '') + entry.id,
52-
children: all(entry.children)
53-
}
54-
]
55-
}
56-
]
57-
})
58-
} else if (tail && tail.type === 'listItem') {
59-
insert(entry, siblings[siblings.length - 1], tight, prefix, ordered)
60-
} else if (tail && tail.type === 'list') {
64+
if (parent.type === 'list') {
65+
if (entry.depth === 1) {
66+
parent.children.push({
67+
type: 'listItem',
68+
spread: false,
69+
children: [
70+
{
71+
type: 'paragraph',
72+
children: [
73+
{
74+
type: 'link',
75+
title: null,
76+
url: '#' + (settings.prefix || '') + entry.id,
77+
children: all(entry.children)
78+
}
79+
]
80+
}
81+
]
82+
})
83+
} else if (parent.children.length > 0) {
84+
insert(entry, parent.children[parent.children.length - 1], settings)
85+
} else {
86+
/** @type {ListItem} */
87+
const item = {type: 'listItem', spread: false, children: []}
88+
parent.children.push(item)
89+
insert(entry, item, settings)
90+
}
91+
}
92+
// List item
93+
else if (
94+
parent.children[parent.children.length - 1] &&
95+
parent.children[parent.children.length - 1].type === 'list'
96+
) {
6197
entry.depth--
62-
insert(entry, tail, tight, prefix, ordered)
63-
} else if (parent.type === 'list') {
64-
const item = {type: 'listItem', spread: false, children: []}
65-
siblings.push(item)
66-
insert(entry, item, tight, prefix, ordered)
98+
insert(
99+
entry,
100+
// @ts-ignore It’s a `list`, we just checked.
101+
parent.children[parent.children.length - 1],
102+
settings
103+
)
67104
} else {
105+
/** @type {List} */
68106
const item = {
69107
type: 'list',
70-
ordered,
108+
ordered: settings.ordered,
71109
spread: false,
72110
children: []
73111
}
74-
siblings.push(item)
112+
parent.children.push(item)
75113
entry.depth--
76-
insert(entry, item, tight, prefix, ordered)
114+
insert(entry, item, settings)
77115
}
78116

79-
if (parent.type === 'list' && !tight) {
117+
if (parent.type === 'list' && !settings.tight) {
80118
parent.spread = false
81119

82-
while (++index < siblings.length) {
83-
if (siblings[index].children.length > 1) {
120+
while (++index < parent.children.length) {
121+
if (parent.children[index].children.length > 1) {
84122
parent.spread = true
85123
break
86124
}
87125
}
88126
} else {
89-
parent.spread = !tight
127+
parent.spread = !settings.tight
90128
}
91129
}
92130

93-
function all(children) {
131+
/**
132+
* @param {Array.<PhrasingContent>} [nodes]
133+
* @returns {Array.<StaticPhrasingContent>}
134+
*/
135+
function all(nodes) {
136+
/** @type {Array.<StaticPhrasingContent>} */
94137
let result = []
95138
let index = -1
96139

97-
if (children) {
98-
while (++index < children.length) {
99-
result = result.concat(one(children[index]))
140+
if (nodes) {
141+
while (++index < nodes.length) {
142+
result = result.concat(one(nodes[index]))
100143
}
101144
}
102145

103146
return result
104147
}
105148

149+
/**
150+
* @param {PhrasingContent} node
151+
* @returns {StaticPhrasingContent|Array.<StaticPhrasingContent>}
152+
*/
106153
function one(node) {
107154
if (
108155
node.type === 'link' ||
109156
node.type === 'linkReference' ||
110157
node.type === 'footnote' ||
111158
node.type === 'footnoteReference'
112159
) {
160+
// @ts-ignore Looks like a parent.
113161
return all(node.children)
114162
}
115163

116164
let copy = extend({}, node)
117-
118165
delete copy.children
119166
delete copy.position
120167

121168
copy = extend(true, {}, copy)
122169

123170
if (node.children) {
171+
// @ts-ignore Looks like a parent.
124172
copy.children = all(node.children)
125173
}
126174

lib/index.js

+27-17
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
1+
/**
2+
* @typedef {import('unist').Node} Node
3+
* @typedef {import('mdast').List} List
4+
* @typedef {import('./search.js').SearchOptions} SearchOptions
5+
* @typedef {import('./contents.js').ContentsOptions} ContentsOptions
6+
* @typedef {SearchOptions & ContentsOptions & ExtraOptions} Options
7+
*
8+
* @typedef ExtraOptions
9+
* @property {string} [heading] Heading to look for, wrapped in `new RegExp('^(' + value + ')$', 'i')`.
10+
*
11+
* @typedef Result
12+
* @property {number} index
13+
* @property {number} endIndex
14+
* @property {List} map
15+
*/
16+
117
import {search} from './search.js'
218
import {contents} from './contents.js'
319
import {toExpression} from './to-expression.js'
420

5-
// Get a TOC representation of `node`.
21+
/**
22+
* Get a TOC representation of `node`.
23+
*
24+
* @param {Node} node
25+
* @param {Options} [options]
26+
* @returns {Result}
27+
*/
628
export function toc(node, options) {
729
const settings = options || {}
830
const heading = settings.heading ? toExpression(settings.heading) : null
931
const result = search(node, heading, settings)
1032

11-
result.map =
12-
result.map.length > 0
13-
? contents(
14-
result.map,
15-
settings.tight,
16-
settings.prefix,
17-
settings.ordered || false
18-
)
19-
: null
20-
21-
// No given heading.
22-
if (!heading) {
23-
result.endIndex = null
24-
result.index = null
33+
return {
34+
index: heading ? result.index : null,
35+
endIndex: heading ? result.endIndex : null,
36+
map: result.map.length > 0 ? contents(result.map, settings) : null
2537
}
26-
27-
return result
2838
}

lib/search.js

+43-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
/**
2+
* @typedef {import('unist').Node} Node
3+
* @typedef {import('mdast').Heading} Heading
4+
* @typedef {import('mdast').PhrasingContent} PhrasingContent
5+
* @typedef {import('unist-util-visit').Visitor<Heading>} HeadingVisitor
6+
* @typedef {import('unist-util-is').Type} IsType
7+
* @typedef {import('unist-util-is').Props} IsProps
8+
* @typedef {import('unist-util-is').TestFunctionAnything} IsTestFunctionAnything
9+
*
10+
* @typedef SearchOptions
11+
* @property {string} [skip] Headings to skip, wrapped in `new RegExp('^(' + value + ')$', 'i')`. Any heading matching this expression will not be present in the table of contents.
12+
* @property {IsType|IsProps|IsTestFunctionAnything|Array.<IsType|IsProps|IsTestFunctionAnything>} [parents]
13+
* @property {Heading['depth']} [maxDepth=6] Maximum heading depth to include in the table of contents. This is inclusive: when set to `3`, level three headings are included (those with three hashes, `###`).
14+
*
15+
* @typedef SearchEntry
16+
* @property {Heading['depth']} depth
17+
* @property {Array.<PhrasingContent>} children
18+
* @property {string} id
19+
*
20+
* @typedef SearchResult
21+
* @property {number} index
22+
* @property {number} endIndex
23+
* @property {Array.<SearchEntry>} map
24+
*/
25+
126
import Slugger from 'github-slugger'
227
import {toString} from 'mdast-util-to-string'
328
import {visit} from 'unist-util-visit'
@@ -6,33 +31,45 @@ import {toExpression} from './to-expression.js'
631

732
const slugs = new Slugger()
833

9-
// Search a node for a location.
34+
/**
35+
* Search a node for a toc.
36+
*
37+
* @param {Node} root
38+
* @param {RegExp} expression
39+
* @param {SearchOptions} settings
40+
* @returns {SearchResult}
41+
*/
1042
export function search(root, expression, settings) {
1143
const skip = settings.skip && toExpression(settings.skip)
1244
const parents = convert(settings.parents || root)
45+
/** @type {Array.<SearchEntry>} */
1346
const map = []
47+
/** @type {number} */
1448
let index
49+
/** @type {number} */
1550
let endIndex
51+
/** @type {Heading} */
1652
let opening
1753

1854
slugs.reset()
1955

2056
// Visit all headings in `root`. We `slug` all headings (to account for
21-
// duplicates), but only create a TOC from top-level headings.
57+
// duplicates), but only create a TOC from top-level headings (by default).
2258
visit(root, 'heading', onheading)
2359

2460
return {
2561
index: index || -1,
2662
// <sindresorhus/eslint-plugin-unicorn#980>
27-
// eslint-disable-next-line unicorn/explicit-length-check
28-
endIndex: index ? endIndex || root.children.length : -1,
63+
// @ts-ignore Looks like a parent.
64+
endIndex: index ? endIndex || root.children.length : -1, // eslint-disable-line unicorn/explicit-length-check
2965
map
3066
}
3167

68+
/** @type {HeadingVisitor} */
3269
function onheading(node, position, parent) {
3370
const value = toString(node, {includeImageAlt: false})
34-
// Remove this when `remark-attr` is up to date w/ micromark.
35-
/* c8 ignore next */
71+
/** @type {string} */
72+
// @ts-ignore `hProperties` from <https://github.com/syntax-tree/mdast-util-to-hast>
3673
const id = node.data && node.data.hProperties && node.data.hProperties.id
3774
const slug = slugs.slug(id || value)
3875

lib/to-expression.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
// Transform a string into an applicable expression.
1+
/**
2+
* Transform a string into an applicable expression.
3+
*
4+
* @param {string} value
5+
* @returns {RegExp}
6+
*/
27
export function toExpression(value) {
38
return new RegExp('^(' + value + ')$', 'i')
49
}

0 commit comments

Comments
 (0)