@0dep/toc 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +73 -14
- package/bin/toc.js +56 -20
- package/index.js +48 -15
- package/package.json +11 -16
- package/types/index.d.ts +79 -69
- package/types/index.d.ts.map +1 -26
- package/index.cjs +0 -410
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
<!-- toc levels="2" -->
|
|
6
|
+
|
|
7
|
+
- [v2.0.0 - 2026-09-18](#v200---2026-09-18)
|
|
8
|
+
- [v1.1.0 - 2026-09-17](#v110---2026-09-17)
|
|
9
|
+
- [v1.0.1 - 2026-09-12](#v101---2026-09-12)
|
|
10
|
+
- [v1.0.0 - 2026-09-12](#v100---2026-09-12)
|
|
11
|
+
- [v0.0.1 - 2026-09-12](#v001---2026-09-12)
|
|
12
|
+
|
|
13
|
+
<!-- /toc -->
|
|
14
|
+
|
|
15
|
+
## v2.0.0 - 2026-09-18
|
|
16
|
+
|
|
17
|
+
### Breaking
|
|
18
|
+
|
|
19
|
+
- Node 22.12 or later. The bin expands glob patterns with `fs.promises.glob`, and `require('@0dep/toc')` loads `index.js` itself, so the CommonJS bundle `index.cjs` is gone
|
|
20
|
+
- `types/index.d.ts` is emitted by `tsc` as a plain module declaration instead of the `declare module '@0dep/toc'` block dts-buddy wrote, with a declaration map back to `index.js`
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `levels="2"`, `levels="2-3"`, `levels="-3"` or `levels="3-"` on the start marker limits the toc to those heading levels, so a changelog can list its versions alone. A broken value is ignored with a warning, `findMarkers` reports it as `warning`. `renderToc` takes `{ levels: '2-3' }`
|
|
25
|
+
- an argument with `*`, `?` or `[` is a glob pattern, `**` matches subdirectories and `node_modules` is never entered, so `toc 'docs/**/*.md'` works the same in an npm script on Windows as on macOS and Linux. A pattern that matches nothing is skipped with a warning, and a file is processed once however many arguments name or match it
|
|
26
|
+
- `-s`/`--silent` drops the status lines and the skipped warnings from the bin, so only anchor warnings and errors are written
|
|
27
|
+
- a file that does not exist is skipped with a warning and no longer sets the exit code, like a file without markers. `--silent` keeps that warning
|
|
28
|
+
|
|
5
29
|
## v1.1.0 - 2026-09-17
|
|
6
30
|
|
|
7
31
|
- duplicate slugs are numbered over the whole document, headings above the start marker included, the way GitHub does. A toc under a heading that shares its name with a later heading used to link the later one without the `-1`
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/zerodep/toc/actions/workflows/build.yaml)[](https://github.com/zerodep/toc/actions/workflows/build-windows.yaml)[](https://coveralls.io/github/zerodep/toc?branch=main)
|
|
4
4
|
|
|
5
|
-
Generate a GitHub flavoured table of contents for markdown files. No dependencies
|
|
5
|
+
Generate a GitHub flavoured table of contents for markdown files. No dependencies. The library is string in, string out and runs in the browser too. Node 22.12 or later for the `toc` bin and for `require`.
|
|
6
6
|
|
|
7
7
|
The toc is written between `<!-- toc -->` and `<!-- /toc -->` markers, and only there. Nothing outside the markers is ever touched, nothing is inserted or guessed. Slugs match GitHub's anchors, and every link to an anchor in the document is checked against them.
|
|
8
8
|
|
|
@@ -12,6 +12,9 @@ The toc is written between `<!-- toc -->` and `<!-- /toc -->` markers, and only
|
|
|
12
12
|
- [Markers](#markers)
|
|
13
13
|
- [Several tocs in one document](#several-tocs-in-one-document)
|
|
14
14
|
- [Options](#options)
|
|
15
|
+
- [`collapsible` and `collapsed`](#collapsible-and-collapsed)
|
|
16
|
+
- [Summary attributes](#summary-attributes)
|
|
17
|
+
- [`levels`](#levels)
|
|
15
18
|
- [What is left alone](#what-is-left-alone)
|
|
16
19
|
- [CLI](#cli)
|
|
17
20
|
- [Options](#options-1)
|
|
@@ -101,6 +104,18 @@ The top toc lists API, get, set and License. The toc under API lists get, set an
|
|
|
101
104
|
|
|
102
105
|
### Options
|
|
103
106
|
|
|
107
|
+
The start marker takes options, bare or as `name="value"`, in any order. It is kept exactly as written, and spacing inside the comment does not matter. Anything on it that is not one of the three options or a well-formed attribute makes the pair skip with a warning rather than guess: a bare name other than the options, so a typo like `collapsable` is caught, an unquoted value like `collapsed=yes`, both details options at once, or attributes without an option to give them a summary element.
|
|
108
|
+
|
|
109
|
+
<!-- toc levels="4" -->
|
|
110
|
+
|
|
111
|
+
- [`collapsible` and `collapsed`](#collapsible-and-collapsed)
|
|
112
|
+
- [Summary attributes](#summary-attributes)
|
|
113
|
+
- [`levels`](#levels)
|
|
114
|
+
|
|
115
|
+
<!-- /toc -->
|
|
116
|
+
|
|
117
|
+
#### `collapsible` and `collapsed`
|
|
118
|
+
|
|
104
119
|
GitHub and most other renderers support the html details element in markdown. Ask for it on the start marker and the list is wrapped in one. `collapsible` starts open and can be folded away, `collapsed` starts folded until the reader clicks the summary. This README has a `collapsed` one under [API](#api) and a `collapsible` one under [Headings](#headings).
|
|
105
120
|
|
|
106
121
|
```markdown
|
|
@@ -122,7 +137,11 @@ becomes
|
|
|
122
137
|
<!-- /toc -->
|
|
123
138
|
```
|
|
124
139
|
|
|
125
|
-
Both take a summary text, `<!-- toc collapsible="Contents" -->` or `<!-- toc collapsed="Contents" -->`.
|
|
140
|
+
Both take a summary text, `<!-- toc collapsible="Contents" -->` or `<!-- toc collapsed="Contents" -->`.
|
|
141
|
+
|
|
142
|
+
#### Summary attributes
|
|
143
|
+
|
|
144
|
+
Any other `name="value"` pair on the start marker is put on the summary element, in the order written:
|
|
126
145
|
|
|
127
146
|
```markdown
|
|
128
147
|
<!-- toc collapsed="Contents" title="Click to expand" style="font-weight: bold" -->
|
|
@@ -135,9 +154,29 @@ Both take a summary text, `<!-- toc collapsible="Contents" -->` or `<!-- toc col
|
|
|
135
154
|
<!-- /toc -->
|
|
136
155
|
```
|
|
137
156
|
|
|
138
|
-
Renderers sanitise html, so check what yours keeps. GitHub drops `style`, `class` and `id` but keeps `title`, `dir`, `lang`, `role` and `aria-*` attributes, so in the example above only the `title` survives there.
|
|
157
|
+
Renderers sanitise html, so check what yours keeps. GitHub drops `style`, `class` and `id` but keeps `title`, `dir`, `lang`, `role` and `aria-*` attributes, so in the example above only the `title` survives there.
|
|
158
|
+
|
|
159
|
+
#### `levels`
|
|
160
|
+
|
|
161
|
+
`levels` limits the toc to a heading level or a range of levels, `levels="2"`, `levels="2-3"`, `levels="-3"` for everything up to `###` or `levels="3-"` for `###` and deeper. The levels are the heading's own, the number of `#`, wherever the marker sits, so a toc under `## API` that should list the `###` headings beneath it says `levels="3"`. The list starts flat at the shallowest level listed, so a changelog with `### Added` and `### Breaking` under every version gets a toc of the versions alone:
|
|
162
|
+
|
|
163
|
+
```markdown
|
|
164
|
+
<!-- toc levels="2" collapsed="Versions" -->
|
|
165
|
+
<details>
|
|
166
|
+
<summary>Versions</summary>
|
|
167
|
+
|
|
168
|
+
- [v2.0.0 - 2026-09-18](#v200---2026-09-18)
|
|
169
|
+
- [v1.1.0 - 2026-09-17](#v110---2026-09-17)
|
|
170
|
+
|
|
171
|
+
</details>
|
|
172
|
+
<!-- /toc -->
|
|
173
|
+
|
|
174
|
+
## v2.0.0 - 2026-09-18
|
|
175
|
+
|
|
176
|
+
### Breaking
|
|
177
|
+
```
|
|
139
178
|
|
|
140
|
-
|
|
179
|
+
A `levels` value that is not a level or a range between 1 and 6 is ignored with a warning, and the pair lists every level. The toc above, under Options, is a `levels="4"` one: without it every heading to the end of this file would be listed.
|
|
141
180
|
|
|
142
181
|
### What is left alone
|
|
143
182
|
|
|
@@ -159,20 +198,23 @@ This README has two headings named Options, the one above and the one under CLI.
|
|
|
159
198
|
npx toc # updates README.md in the current directory
|
|
160
199
|
npx toc docs/a.md docs/b.md # several files
|
|
161
200
|
npx toc docs/a.md,docs/b.md # comma separated works too
|
|
201
|
+
npx toc 'docs/**/*.md' # glob patterns, quoted so the shell leaves them alone
|
|
162
202
|
npx toc --dry-run README.md # print the toc, write nothing
|
|
163
203
|
npx toc --check README.md # exit with 1 when a link to an anchor has no target
|
|
204
|
+
npx toc -c -s docs/*.md # write only the anchor warnings
|
|
164
205
|
npx toc --help
|
|
165
206
|
```
|
|
166
207
|
|
|
167
|
-
Paths are resolved against the current directory.
|
|
208
|
+
Paths are resolved against the current directory. An argument with `*`, `?` or `[` is a glob pattern, expanded by the bin with Node's own `fs.glob`, so it works the same in an npm script on Windows, where the shell does not expand it, as on macOS and Linux. `**` matches subdirectories, `node_modules` is never entered, and the matches are processed in sorted order. A pattern that matches nothing is skipped with a warning. A file is processed once, however many arguments name or match it.
|
|
168
209
|
|
|
169
210
|
### Options
|
|
170
211
|
|
|
171
|
-
| Option | Effect
|
|
172
|
-
| --------------- |
|
|
173
|
-
| `-n, --dry-run` | Print the toc of every pair to stdout and the status to stderr, write nothing
|
|
174
|
-
| `-c, --check` | Exit with 1 when a link to an anchor has no target
|
|
175
|
-
| `-
|
|
212
|
+
| Option | Effect |
|
|
213
|
+
| --------------- | ---------------------------------------------------------------------------------------------- |
|
|
214
|
+
| `-n, --dry-run` | Print the toc of every pair to stdout and the status to stderr, write nothing |
|
|
215
|
+
| `-c, --check` | Exit with 1 when a link to an anchor has no target |
|
|
216
|
+
| `-s, --silent` | Skip the status lines and the skipped warnings, keep missing files, anchor warnings and errors |
|
|
217
|
+
| `-h, --help` | Show usage |
|
|
176
218
|
|
|
177
219
|
### Output and exit code
|
|
178
220
|
|
|
@@ -182,6 +224,8 @@ One status line per file on stdout, and one warning per skipped file or pair on
|
|
|
182
224
|
| --------------------------------------------------------------------------------- | ------ | ---------------------------------------------------- |
|
|
183
225
|
| `README.md: wrote TOC.` | stdout | At least one pair changed and the file was written |
|
|
184
226
|
| `README.md: TOC already up to date.` | stdout | Every updatable pair was already correct |
|
|
227
|
+
| `README.md: no such file, skipped.` | stderr | The file does not exist |
|
|
228
|
+
| `docs/*.md: no matching file, skipped.` | stderr | The pattern matched nothing |
|
|
185
229
|
| `README.md: no TOC markers, skipped.` | stderr | The file has no markers |
|
|
186
230
|
| `README.md:3: TOC start marker without end marker, skipped.` | stderr | Unbalanced pair |
|
|
187
231
|
| `README.md:3: TOC end marker without start marker, skipped.` | stderr | Unbalanced pair |
|
|
@@ -189,9 +233,10 @@ One status line per file on stdout, and one warning per skipped file or pair on
|
|
|
189
233
|
| `README.md:3: unknown TOC option foo, skipped.` | stderr | The start marker has something that is not an option |
|
|
190
234
|
| `README.md:3: TOC options collapsible and collapsed exclude each other, skipped.` | stderr | Pick one |
|
|
191
235
|
| `README.md:3: TOC attributes style need collapsible or collapsed, skipped.` | stderr | There is no summary element to put them on |
|
|
236
|
+
| `README.md:3: TOC option levels "x" is not a level or a range like 2-3, ignored.` | stderr | Every level is listed instead |
|
|
192
237
|
| `README.md:9: anchor #instal has no target.` | stderr | A link points at a heading that does not exist |
|
|
193
238
|
| `README.md:9: anchor #Install has no target, did you mean #install?` | stderr | Same, and a heading obviously matches |
|
|
194
|
-
| `README.md:
|
|
239
|
+
| `README.md: EISDIR: illegal operation on a directory, ...` | stderr | The file could not be read or written |
|
|
195
240
|
|
|
196
241
|
The exit code is 1 when a file could not be read or written, when an option is not recognised, or, with `--check`, when a link to an anchor has no target. Otherwise 0. Skipped files and pairs do not affect the exit code.
|
|
197
242
|
|
|
@@ -205,6 +250,12 @@ The warning ends with `did you mean` when the target is obvious: slugging the an
|
|
|
205
250
|
npx toc --check docs/*.md
|
|
206
251
|
```
|
|
207
252
|
|
|
253
|
+
With `--silent` the status lines and the skipped warnings are dropped, so the output is the missing files, the anchor warnings and the errors alone. In a dry run the toc blocks are still printed to stdout.
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
npx toc --check --silent docs/*.md
|
|
257
|
+
```
|
|
258
|
+
|
|
208
259
|
### Dry run
|
|
209
260
|
|
|
210
261
|
With `--dry-run` nothing is written. The rendered block of every start marker goes to stdout so it can be piped or redirected, and the status line and warnings go to stderr. A start marker that would be skipped, unbalanced or with a problem, still gets its block printed, plain when its options are broken, next to the warning. A file without markers gets a block with every heading, so the output can be pasted into the document as a starting point.
|
|
@@ -260,7 +311,7 @@ To regenerate the toc and format the rest of the file in one go, let a `posttoc`
|
|
|
260
311
|
import { buildToc, renderToc, findMarkers, findAnchors, slugify, headingText, TOC_START, TOC_END } from '@0dep/toc';
|
|
261
312
|
```
|
|
262
313
|
|
|
263
|
-
CommonJS works the same way with `require('@0dep/toc')
|
|
314
|
+
CommonJS works the same way with `require('@0dep/toc')`, there is no separate bundle since Node 22.12 requires ES modules natively.
|
|
264
315
|
|
|
265
316
|
### `buildToc(source)`
|
|
266
317
|
|
|
@@ -290,7 +341,7 @@ console.log(md);
|
|
|
290
341
|
|
|
291
342
|
### `renderToc(source[, fromLine[, options]])`
|
|
292
343
|
|
|
293
|
-
Returns the toc block, markers included, for the headings below the zero based line `fromLine`, typically a start marker's line. By default every heading is listed. Returns an empty string when there is nothing to list. `options` are rendered into the start marker and the block, `{ collapsible: true }`, `{ collapsed: 'Contents', attributes: { class: 'toc' } }` and so on.
|
|
344
|
+
Returns the toc block, markers included, for the headings below the zero based line `fromLine`, typically a start marker's line. By default every heading is listed. Returns an empty string when there is nothing to list. `options` are rendered into the start marker and the block, `{ collapsible: true }`, `{ collapsed: 'Contents', attributes: { class: 'toc' } }`, `{ levels: '2-3' }` and so on.
|
|
294
345
|
|
|
295
346
|
```javascript
|
|
296
347
|
import { renderToc } from '@0dep/toc';
|
|
@@ -310,7 +361,7 @@ console.log(renderToc('# Title\n\n## Install\n\n### From npm\n'));
|
|
|
310
361
|
|
|
311
362
|
### `findMarkers(source)`
|
|
312
363
|
|
|
313
|
-
Returns every marker pair in document order as `{ start, end, options }`, zero based line numbers outside fenced code blocks and the recognised options written on the start marker. A start marker pairs with the first end marker after it. A missing side is `-1`, a start marker without an end marker or an end marker with no open start before it. `options.attributes` holds the other `name="value"` pairs, when there are any. When the start marker cannot be used, `problem` says why and is otherwise absent.
|
|
364
|
+
Returns every marker pair in document order as `{ start, end, options }`, zero based line numbers outside fenced code blocks and the recognised options written on the start marker. A start marker pairs with the first end marker after it. A missing side is `-1`, a start marker without an end marker or an end marker with no open start before it. `options.attributes` holds the other `name="value"` pairs, when there are any. When the start marker cannot be used, `problem` says why and is otherwise absent. When an option on it is ignored, a broken `levels` value, `warning` says so.
|
|
314
365
|
|
|
315
366
|
```javascript
|
|
316
367
|
import { findMarkers } from '@0dep/toc';
|
|
@@ -389,6 +440,14 @@ The marker strings, `<!-- toc -->` and `<!-- /toc -->`, for code that wants to l
|
|
|
389
440
|
</details>
|
|
390
441
|
<!-- /toc -->
|
|
391
442
|
|
|
443
|
+
And one with `levels="2"`, which lists only the level 2 headings that follow:
|
|
444
|
+
|
|
445
|
+
<!-- toc levels="2" -->
|
|
446
|
+
|
|
447
|
+
- [License](#license)
|
|
448
|
+
|
|
449
|
+
<!-- /toc -->
|
|
450
|
+
|
|
392
451
|
### What is listed
|
|
393
452
|
|
|
394
453
|
- ATX headings, `## Heading`, with up to three leading spaces and a space after the hashes, and setext headings underlined with `===` or `---`
|
package/bin/toc.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { glob, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, resolve } from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { buildToc, findAnchors, findMarkers, renderToc } from '../index.js';
|
|
6
7
|
|
|
@@ -8,9 +9,9 @@ const USAGE = `Usage: toc [options] [file...]
|
|
|
8
9
|
|
|
9
10
|
Update the table of contents between every <!-- toc --> and <!-- /toc -->
|
|
10
11
|
pair in markdown files. Each pair lists the headings below its own start
|
|
11
|
-
marker. A file
|
|
12
|
-
unbalanced, has a problem on its start marker, or has
|
|
13
|
-
it, each with a warning on stderr. Every link to an anchor, [text](#slug),
|
|
12
|
+
marker. A file that does not exist or has no markers is skipped, and so
|
|
13
|
+
is a pair that is unbalanced, has a problem on its start marker, or has
|
|
14
|
+
no headings below it, each with a warning on stderr. Every link to an anchor, [text](#slug),
|
|
14
15
|
is checked against the headings and html ids in the file and a link
|
|
15
16
|
without a target gets a warning too.
|
|
16
17
|
|
|
@@ -20,17 +21,23 @@ Markers:
|
|
|
20
21
|
<!-- toc collapsed --> same, but closed until clicked
|
|
21
22
|
<!-- toc collapsed="Contents" --> either one with a custom summary text
|
|
22
23
|
<!-- toc collapsed class="toc" --> other name="value" pairs go on <summary>
|
|
24
|
+
<!-- toc levels="2-3" --> only these heading levels, also "2", "-3", "3-"
|
|
23
25
|
<!-- /toc --> end of the block
|
|
24
26
|
|
|
25
27
|
Files are resolved against the current directory and default to
|
|
26
28
|
README.md. Several files can be given as separate arguments or comma
|
|
27
|
-
separated.
|
|
29
|
+
separated. An argument with *, ? or [ is a glob pattern, ** matches
|
|
30
|
+
subdirectories, node_modules is never entered. A pattern that matches
|
|
31
|
+
nothing is skipped with a warning. A file is processed once, however
|
|
32
|
+
many arguments name or match it.
|
|
28
33
|
|
|
29
34
|
Options:
|
|
30
35
|
-n, --dry-run print the toc to stdout and report on stderr, write nothing.
|
|
31
36
|
Without markers every heading is listed so the block can be
|
|
32
37
|
pasted into the document
|
|
33
38
|
-c, --check exit with 1 when a link to an anchor has no target
|
|
39
|
+
-s, --silent skip the status lines and the skipped warnings, only missing
|
|
40
|
+
files, anchor warnings and errors are written
|
|
34
41
|
-h, --help show this help
|
|
35
42
|
`;
|
|
36
43
|
|
|
@@ -49,27 +56,51 @@ async function main() {
|
|
|
49
56
|
return;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const arg of files) {
|
|
61
|
+
const matches = await expand(arg);
|
|
62
|
+
if (!matches.length) console.error(`${arg}: no matching file, skipped.`);
|
|
63
|
+
for (const file of matches) {
|
|
64
|
+
const key = resolve(file);
|
|
65
|
+
if (seen.has(key)) continue;
|
|
66
|
+
seen.add(key);
|
|
67
|
+
try {
|
|
68
|
+
await processFile(file, flags);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code === 'ENOENT') {
|
|
71
|
+
console.error(`${file}: no such file, skipped.`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
console.error(`${file}: ${/** @type {Error} */ (err).message}`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
79
|
}
|
|
61
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Expand a glob pattern to the sorted matching files, a plain path is returned as is.
|
|
83
|
+
* @param {string} arg
|
|
84
|
+
* @returns {Promise<string[]>}
|
|
85
|
+
*/
|
|
86
|
+
async function expand(arg) {
|
|
87
|
+
if (!/[*?[]/.test(arg)) return [arg];
|
|
88
|
+
const matches = [];
|
|
89
|
+
for await (const file of glob(arg, { exclude: (path) => basename(path) === 'node_modules' })) matches.push(file);
|
|
90
|
+
return matches.sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
62
93
|
/**
|
|
63
94
|
* Update the toc and check the anchors of one file, or report what would happen with `dryRun`.
|
|
64
95
|
* @param {string} file
|
|
65
|
-
* @param {{ dryRun: boolean, check: boolean }} flags
|
|
96
|
+
* @param {{ dryRun: boolean, check: boolean, silent: boolean }} flags
|
|
66
97
|
*/
|
|
67
|
-
async function processFile(file, { dryRun, check }) {
|
|
98
|
+
async function processFile(file, { dryRun, check, silent }) {
|
|
68
99
|
const source = await readFile(file, 'utf8');
|
|
69
|
-
const status = dryRun ? console.error : console.log;
|
|
100
|
+
const status = silent ? noop : dryRun ? console.error : console.log;
|
|
70
101
|
const warn = console.error;
|
|
71
102
|
|
|
72
|
-
const regenerated = reportMarkers(file, source, dryRun);
|
|
103
|
+
const regenerated = reportMarkers(file, source, dryRun, silent);
|
|
73
104
|
let updated = source;
|
|
74
105
|
if (regenerated.length) {
|
|
75
106
|
updated = buildToc(source);
|
|
@@ -91,11 +122,12 @@ async function processFile(file, { dryRun, check }) {
|
|
|
91
122
|
* @param {string} file
|
|
92
123
|
* @param {string} source
|
|
93
124
|
* @param {boolean} dryRun
|
|
125
|
+
* @param {boolean} silent
|
|
94
126
|
* @returns {Array<{ start: number, end: number }>}
|
|
95
127
|
*/
|
|
96
|
-
function reportMarkers(file, source, dryRun) {
|
|
128
|
+
function reportMarkers(file, source, dryRun, silent) {
|
|
97
129
|
const markers = findMarkers(source);
|
|
98
|
-
const warn = console.error;
|
|
130
|
+
const warn = silent ? noop : console.error;
|
|
99
131
|
/** @type {Array<{ start: number, end: number }>} */
|
|
100
132
|
const regenerated = [];
|
|
101
133
|
|
|
@@ -106,11 +138,12 @@ function reportMarkers(file, source, dryRun) {
|
|
|
106
138
|
return regenerated;
|
|
107
139
|
}
|
|
108
140
|
|
|
109
|
-
for (const { start, end, options, problem } of markers) {
|
|
141
|
+
for (const { start, end, options, problem, warning } of markers) {
|
|
110
142
|
if (start === -1) {
|
|
111
143
|
warn(`${file}:${end + 1}: TOC end marker without start marker, skipped.`);
|
|
112
144
|
continue;
|
|
113
145
|
}
|
|
146
|
+
if (warning) warn(`${file}:${start + 1}: ${warning}.`);
|
|
114
147
|
const toc = renderToc(source, start, problem ? {} : options);
|
|
115
148
|
if (dryRun && toc) console.log(toc);
|
|
116
149
|
if (problem) warn(`${file}:${start + 1}: ${problem}, skipped.`);
|
|
@@ -123,18 +156,21 @@ function reportMarkers(file, source, dryRun) {
|
|
|
123
156
|
|
|
124
157
|
/**
|
|
125
158
|
* @param {string[]} argv
|
|
126
|
-
* @returns {{ files: string[], dryRun: boolean, check: boolean, help: boolean, unknown?: string }}
|
|
159
|
+
* @returns {{ files: string[], dryRun: boolean, check: boolean, silent: boolean, help: boolean, unknown?: string }}
|
|
127
160
|
*/
|
|
128
161
|
function parseArgs(argv) {
|
|
129
162
|
/** @type {string[]} */
|
|
130
163
|
const files = [];
|
|
131
|
-
const flags = { dryRun: false, check: false, help: false };
|
|
164
|
+
const flags = { dryRun: false, check: false, silent: false, help: false };
|
|
132
165
|
for (const arg of argv) {
|
|
133
166
|
if (arg === '-n' || arg === '--dry-run') flags.dryRun = true;
|
|
134
167
|
else if (arg === '-c' || arg === '--check') flags.check = true;
|
|
168
|
+
else if (arg === '-s' || arg === '--silent') flags.silent = true;
|
|
135
169
|
else if (arg === '-h' || arg === '--help') flags.help = true;
|
|
136
170
|
else if (arg.startsWith('-')) return { files, ...flags, unknown: arg };
|
|
137
171
|
else files.push(...arg.split(',').filter(Boolean));
|
|
138
172
|
}
|
|
139
173
|
return { files: files.length ? files : ['README.md'], ...flags };
|
|
140
174
|
}
|
|
175
|
+
|
|
176
|
+
function noop() {}
|
package/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export const TOC_START = '<!-- toc -->';
|
|
2
2
|
export const TOC_END = '<!-- /toc -->';
|
|
3
3
|
const DEFAULT_SUMMARY = 'Table of contents';
|
|
4
|
-
const KNOWN_OPTIONS = ['collapsible', 'collapsed'];
|
|
4
|
+
const KNOWN_OPTIONS = ['collapsible', 'collapsed', 'levels'];
|
|
5
|
+
const LEVELS = /^(?:([1-6])|([1-6])-([1-6])|([1-6])-|-([1-6]))$/;
|
|
5
6
|
const NUL = String.fromCharCode(0);
|
|
6
7
|
/** @type {Map<string, ReturnType<typeof analyse>>} */
|
|
7
8
|
const cache = new Map();
|
|
@@ -19,7 +20,7 @@ export function buildToc(source) {
|
|
|
19
20
|
let cursor = 0;
|
|
20
21
|
for (const { start, end, options, problem } of markers) {
|
|
21
22
|
if (start === -1 || end === -1 || problem) continue;
|
|
22
|
-
const listed = headlines
|
|
23
|
+
const listed = headingsBelow(headlines, start, options);
|
|
23
24
|
if (listed.length === 0) continue;
|
|
24
25
|
out.push(...lines.slice(cursor, start), renderBlock(listed, lines[start], options, eol));
|
|
25
26
|
cursor = end + 1;
|
|
@@ -37,7 +38,7 @@ export function buildToc(source) {
|
|
|
37
38
|
*/
|
|
38
39
|
export function renderToc(source, fromLine = -1, options = {}) {
|
|
39
40
|
const { eol, headlines } = analyse(source);
|
|
40
|
-
const listed = headlines
|
|
41
|
+
const listed = headingsBelow(headlines, fromLine, options);
|
|
41
42
|
return listed.length === 0 ? '' : renderBlock(listed, formatMarker(options), options, eol);
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -242,6 +243,33 @@ function assignSlugs(headlines) {
|
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
/**
|
|
247
|
+
* The headings after `fromLine` within the levels the options allow.
|
|
248
|
+
* @param {Headline[]} headlines
|
|
249
|
+
* @param {number} fromLine
|
|
250
|
+
* @param {TocOptions} options
|
|
251
|
+
* @returns {Headline[]}
|
|
252
|
+
*/
|
|
253
|
+
function headingsBelow(headlines, fromLine, options) {
|
|
254
|
+
const [min, max] = levelRange(options.levels) ?? [1, 6];
|
|
255
|
+
return headlines.filter((h) => h.line > fromLine && h.level >= min && h.level <= max);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The lowest and highest heading level a levels option allows, every level when absent, undefined when broken.
|
|
260
|
+
* @param {string} [levels]
|
|
261
|
+
* @returns {[number, number] | undefined}
|
|
262
|
+
*/
|
|
263
|
+
function levelRange(levels) {
|
|
264
|
+
if (levels === undefined) return [1, 6];
|
|
265
|
+
const match = LEVELS.exec(levels);
|
|
266
|
+
if (!match) return undefined;
|
|
267
|
+
const [, only, from, to, fromOpen, toOpen] = match;
|
|
268
|
+
const min = Number(only ?? from ?? fromOpen ?? 1);
|
|
269
|
+
const max = Number(only ?? to ?? toOpen ?? 6);
|
|
270
|
+
return min <= max ? [min, max] : undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
245
273
|
/**
|
|
246
274
|
* The toc block for the headings, between the start marker line and the end marker.
|
|
247
275
|
* @param {Headline[]} headlines
|
|
@@ -266,9 +294,9 @@ function renderBlock(headlines, startLine, options, eol = '\n') {
|
|
|
266
294
|
}
|
|
267
295
|
|
|
268
296
|
/**
|
|
269
|
-
* The options written on a start marker, with a problem when they cannot be used.
|
|
297
|
+
* The options written on a start marker, with a problem when they cannot be used and a warning when one is ignored.
|
|
270
298
|
* @param {string | undefined} text
|
|
271
|
-
* @returns {{ options: TocOptions, problem?: string }}
|
|
299
|
+
* @returns {{ options: TocOptions, problem?: string, warning?: string }}
|
|
272
300
|
*/
|
|
273
301
|
function parseOptions(text) {
|
|
274
302
|
/** @type {TocOptions} */
|
|
@@ -278,20 +306,25 @@ function parseOptions(text) {
|
|
|
278
306
|
/** @type {string[]} */
|
|
279
307
|
const unknown = [];
|
|
280
308
|
for (const [token, name, value] of (text ?? '').matchAll(/([^\s="]+)(?:="([^"]*)")?(?=\s|$)|\S+/g)) {
|
|
281
|
-
if (name
|
|
309
|
+
if (name === 'levels') options.levels = value ?? '';
|
|
310
|
+
else if (name && KNOWN_OPTIONS.includes(name)) options[/** @type {'collapsible' | 'collapsed'} */ (name)] = value ?? true;
|
|
282
311
|
else if (name && value !== undefined && /^[a-zA-Z][\w:.-]*$/.test(name)) attributes[name] = value;
|
|
283
312
|
else unknown.push(token);
|
|
284
313
|
}
|
|
285
314
|
const names = Object.keys(attributes);
|
|
286
315
|
if (names.length) options.attributes = attributes;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
316
|
+
/** @type {{ options: TocOptions, problem?: string, warning?: string }} */
|
|
317
|
+
const parsed = { options };
|
|
318
|
+
if (options.levels !== undefined && !levelRange(options.levels)) {
|
|
319
|
+
parsed.warning = `TOC option levels "${options.levels}" is not a level or a range like 2-3, ignored`;
|
|
290
320
|
}
|
|
291
|
-
if (
|
|
292
|
-
|
|
321
|
+
if (unknown.length) parsed.problem = `unknown TOC option ${unknown.join(' ')}`;
|
|
322
|
+
else if (options.collapsible !== undefined && options.collapsed !== undefined) {
|
|
323
|
+
parsed.problem = 'TOC options collapsible and collapsed exclude each other';
|
|
324
|
+
} else if (names.length && options.collapsible === undefined && options.collapsed === undefined) {
|
|
325
|
+
parsed.problem = `TOC attributes ${names.join(', ')} need collapsible or collapsed`;
|
|
293
326
|
}
|
|
294
|
-
return
|
|
327
|
+
return parsed;
|
|
295
328
|
}
|
|
296
329
|
|
|
297
330
|
/**
|
|
@@ -379,7 +412,7 @@ function isParagraphText(line) {
|
|
|
379
412
|
|
|
380
413
|
/**
|
|
381
414
|
* Options written on a start marker, each `true` or a summary text, and the other attributes for the summary element.
|
|
382
|
-
* @typedef {{ collapsible?: true | string, collapsed?: true | string, attributes?: Record<string, string> }} TocOptions
|
|
415
|
+
* @typedef {{ collapsible?: true | string, collapsed?: true | string, levels?: string, attributes?: Record<string, string> }} TocOptions
|
|
383
416
|
*/
|
|
384
417
|
|
|
385
418
|
/**
|
|
@@ -394,6 +427,6 @@ function isParagraphText(line) {
|
|
|
394
427
|
*/
|
|
395
428
|
|
|
396
429
|
/**
|
|
397
|
-
* A marker pair as zero based lines, -1 for a missing side, with its options
|
|
398
|
-
* @typedef {{ start: number, end: number, options: TocOptions, problem?: string }} Marker
|
|
430
|
+
* A marker pair as zero based lines, -1 for a missing side, with its options, a problem when they cannot be used and a warning when one is ignored.
|
|
431
|
+
* @typedef {{ start: number, end: number, options: TocOptions, problem?: string, warning?: string }} Marker
|
|
399
432
|
*/
|
package/package.json
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0dep/toc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Generate a GitHub flavoured markdown table of contents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"module": "./index.js",
|
|
8
|
-
"main": "./index.cjs",
|
|
9
8
|
"types": "./types/index.d.ts",
|
|
10
9
|
"exports": {
|
|
11
10
|
".": {
|
|
12
11
|
"types": "./types/index.d.ts",
|
|
13
|
-
"
|
|
14
|
-
"import": "./index.js"
|
|
12
|
+
"default": "./index.js"
|
|
15
13
|
}
|
|
16
14
|
},
|
|
17
15
|
"bin": {
|
|
18
16
|
"toc": "bin/toc.js"
|
|
19
17
|
},
|
|
20
18
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
19
|
+
"node": ">=22.12"
|
|
22
20
|
},
|
|
23
21
|
"scripts": {
|
|
24
|
-
"pretest": "npm run toc && npm run
|
|
22
|
+
"pretest": "npm run toc && npm run types",
|
|
25
23
|
"test": "mocha",
|
|
26
24
|
"posttest": "npm run lint && npm run tsc && npm run test:md",
|
|
27
25
|
"test:md": "texample",
|
|
28
|
-
"toc": "node bin/toc.js --check README.md",
|
|
29
|
-
"posttoc": "prettier --write README.md",
|
|
26
|
+
"toc": "node bin/toc.js --check README.md CHANGELOG.md",
|
|
27
|
+
"posttoc": "prettier --write README.md CHANGELOG.md",
|
|
30
28
|
"lint": "eslint . --cache && prettier . --check --cache",
|
|
31
29
|
"tsc": "tsc -p test",
|
|
32
|
-
"
|
|
33
|
-
"prepack": "npm run
|
|
30
|
+
"types": "tsc",
|
|
31
|
+
"prepack": "npm run types",
|
|
34
32
|
"cov:html": "c8 -r html -r text mocha",
|
|
35
33
|
"test:lcov": "c8 -r lcov mocha && npm run lint"
|
|
36
34
|
},
|
|
@@ -46,7 +44,8 @@
|
|
|
46
44
|
"markdown",
|
|
47
45
|
"readme",
|
|
48
46
|
"github",
|
|
49
|
-
"slug"
|
|
47
|
+
"slug",
|
|
48
|
+
"glob"
|
|
50
49
|
],
|
|
51
50
|
"author": {
|
|
52
51
|
"name": "Zerodep AB",
|
|
@@ -63,26 +62,22 @@
|
|
|
63
62
|
"license": "MIT",
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"@eslint/js": "^10.0.1",
|
|
66
|
-
"@rollup/plugin-commonjs": "^29.0.3",
|
|
67
65
|
"@types/chai": "^5.2.3",
|
|
68
66
|
"@types/mocha": "^10.0.10",
|
|
69
67
|
"@types/node": "^22.20.2",
|
|
70
68
|
"c8": "^12.0.0",
|
|
71
69
|
"chai": "^6.2.2",
|
|
72
|
-
"dts-buddy": "^0.8.3",
|
|
73
70
|
"eslint": "^10.10.0",
|
|
74
71
|
"globals": "^17.12.0",
|
|
75
72
|
"mocha": "^12.0.0",
|
|
76
73
|
"prettier": "^3.9.6",
|
|
77
|
-
"rollup": "^4.63.1",
|
|
78
74
|
"texample": "^1.0.2",
|
|
79
75
|
"typescript": "^6.0.3"
|
|
80
76
|
},
|
|
81
77
|
"files": [
|
|
82
78
|
"bin/",
|
|
83
79
|
"index.js",
|
|
84
|
-
"
|
|
85
|
-
"types/index.d.ts*",
|
|
80
|
+
"types/",
|
|
86
81
|
"CHANGELOG.md"
|
|
87
82
|
]
|
|
88
83
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,70 +1,80 @@
|
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The markdown with the toc between every `<!-- toc -->` and `<!-- /toc -->` pair regenerated.
|
|
3
|
+
* @param {string} source
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
export function buildToc(source: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* The toc block, markers included, for the headings below `fromLine`, empty when there is nothing to list.
|
|
9
|
+
* @param {string} source
|
|
10
|
+
* @param {number} [fromLine] zero based
|
|
11
|
+
* @param {TocOptions} [options]
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function renderToc(source: string, fromLine?: number, options?: TocOptions): string;
|
|
15
|
+
/**
|
|
16
|
+
* Every marker pair in document order, shared with later calls for the same source.
|
|
17
|
+
* @param {string} source
|
|
18
|
+
* @returns {Marker[]}
|
|
19
|
+
*/
|
|
20
|
+
export function findMarkers(source: string): Marker[];
|
|
21
|
+
/**
|
|
22
|
+
* Every link to an anchor in the document, in order, shared with later calls for the same source.
|
|
23
|
+
* @param {string} source
|
|
24
|
+
* @returns {Anchor[]}
|
|
25
|
+
*/
|
|
26
|
+
export function findAnchors(source: string): Anchor[];
|
|
27
|
+
/**
|
|
28
|
+
* The GitHub anchor slug of a heading's rendered text.
|
|
29
|
+
* @param {string} text
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function slugify(text: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* The text GitHub renders for a heading's inline markdown.
|
|
35
|
+
* @param {string} markdown
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function headingText(markdown: string): string;
|
|
39
|
+
export const TOC_START: "<!-- toc -->";
|
|
40
|
+
export const TOC_END: "<!-- /toc -->";
|
|
41
|
+
/**
|
|
42
|
+
* Options written on a start marker, each `true` or a summary text, and the other attributes for the summary element.
|
|
43
|
+
*/
|
|
44
|
+
export type TocOptions = {
|
|
45
|
+
collapsible?: true | string;
|
|
46
|
+
collapsed?: true | string;
|
|
47
|
+
levels?: string;
|
|
48
|
+
attributes?: Record<string, string>;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A heading with its zero based line, level, inline markdown and GitHub slug.
|
|
52
|
+
*/
|
|
53
|
+
export type Headline = {
|
|
54
|
+
line: number;
|
|
55
|
+
level: number;
|
|
56
|
+
markdown: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A link to an anchor with its zero based line, text, anchor as written, whether it has a target and, when it
|
|
61
|
+
* has none and one heading clearly matches, a suggestion.
|
|
62
|
+
*/
|
|
63
|
+
export type Anchor = {
|
|
64
|
+
line: number;
|
|
65
|
+
text: string;
|
|
66
|
+
anchor: string;
|
|
67
|
+
valid: boolean;
|
|
68
|
+
suggestion?: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* A marker pair as zero based lines, -1 for a missing side, with its options, a problem when they cannot be used and a warning when one is ignored.
|
|
72
|
+
*/
|
|
73
|
+
export type Marker = {
|
|
74
|
+
start: number;
|
|
75
|
+
end: number;
|
|
76
|
+
options: TocOptions;
|
|
77
|
+
problem?: string;
|
|
78
|
+
warning?: string;
|
|
79
|
+
};
|
|
70
80
|
//# sourceMappingURL=index.d.ts.map
|
package/types/index.d.ts.map
CHANGED
|
@@ -1,26 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"file": "index.d.ts",
|
|
4
|
-
"names": [
|
|
5
|
-
"buildToc",
|
|
6
|
-
"renderToc",
|
|
7
|
-
"findMarkers",
|
|
8
|
-
"findAnchors",
|
|
9
|
-
"slugify",
|
|
10
|
-
"headingText",
|
|
11
|
-
"TOC_START",
|
|
12
|
-
"TOC_END",
|
|
13
|
-
"TocOptions",
|
|
14
|
-
"Headline",
|
|
15
|
-
"Anchor",
|
|
16
|
-
"Marker"
|
|
17
|
-
],
|
|
18
|
-
"sources": [
|
|
19
|
-
"../index.js"
|
|
20
|
-
],
|
|
21
|
-
"sourcesContent": [
|
|
22
|
-
null
|
|
23
|
-
],
|
|
24
|
-
"mappings": ";;;;iBAcgBA,QAAQA;;;;;iBAuBRC,SAASA;;;;iBAWTC,WAAWA;;;;iBASXC,WAAWA;;;;iBASXC,OAAOA;;;;iBAYPC,WAAWA;cA9EdC,SAASA;cACTC,OAAOA;;;;aA4X0FC,UAAUA;;;;;;;;aAK1CC,QAAQA;;;;;;;;;;aAMYC,MAAMA;;;;;;;;;;aAKpBC,MAAMA",
|
|
25
|
-
"ignoreList": []
|
|
26
|
-
}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.js"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,iCAHW,MAAM,GACJ,MAAM,CAgBlB;AAED;;;;;;GAMG;AACH,kCALW,MAAM,aACN,MAAM,YACN,UAAU,GACR,MAAM,CAMlB;AAED;;;;GAIG;AACH,oCAHW,MAAM,GACJ,MAAM,EAAE,CAIpB;AAED;;;;GAIG;AACH,oCAHW,MAAM,GACJ,MAAM,EAAE,CAIpB;AAED;;;;GAIG;AACH,8BAHW,MAAM,GACJ,MAAM,CAOlB;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAUlB;AAvFD,wBAAyB,cAAc,CAAC;AACxC,sBAAuB,eAAe,CAAC;;;;yBA6Z1B;IAAE,WAAW,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE;;;;uBAKhH;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;;;;;qBAM/D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE;;;;qBAKnF;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,UAAU,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE"}
|
package/index.cjs
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const TOC_START = '<!-- toc -->';
|
|
4
|
-
const TOC_END = '<!-- /toc -->';
|
|
5
|
-
const DEFAULT_SUMMARY = 'Table of contents';
|
|
6
|
-
const KNOWN_OPTIONS = ['collapsible', 'collapsed'];
|
|
7
|
-
const NUL = String.fromCharCode(0);
|
|
8
|
-
/** @type {Map<string, ReturnType<typeof analyse>>} */
|
|
9
|
-
const cache = new Map();
|
|
10
|
-
const LINK = /\[([^\]]*)\]\(#([^)\s]+)/g;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The markdown with the toc between every `<!-- toc -->` and `<!-- /toc -->` pair regenerated.
|
|
14
|
-
* @param {string} source
|
|
15
|
-
* @returns {string}
|
|
16
|
-
*/
|
|
17
|
-
function buildToc(source) {
|
|
18
|
-
const { bom, lines, eol, headlines, markers } = analyse(source);
|
|
19
|
-
/** @type {string[]} */
|
|
20
|
-
const out = [];
|
|
21
|
-
let cursor = 0;
|
|
22
|
-
for (const { start, end, options, problem } of markers) {
|
|
23
|
-
if (start === -1 || end === -1 || problem) continue;
|
|
24
|
-
const listed = headlines.filter((h) => h.line > start);
|
|
25
|
-
if (listed.length === 0) continue;
|
|
26
|
-
out.push(...lines.slice(cursor, start), renderBlock(listed, lines[start], options, eol));
|
|
27
|
-
cursor = end + 1;
|
|
28
|
-
}
|
|
29
|
-
out.push(...lines.slice(cursor));
|
|
30
|
-
return bom + out.join(eol);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* The toc block, markers included, for the headings below `fromLine`, empty when there is nothing to list.
|
|
35
|
-
* @param {string} source
|
|
36
|
-
* @param {number} [fromLine] zero based
|
|
37
|
-
* @param {TocOptions} [options]
|
|
38
|
-
* @returns {string}
|
|
39
|
-
*/
|
|
40
|
-
function renderToc(source, fromLine = -1, options = {}) {
|
|
41
|
-
const { eol, headlines } = analyse(source);
|
|
42
|
-
const listed = headlines.filter((h) => h.line > fromLine);
|
|
43
|
-
return listed.length === 0 ? '' : renderBlock(listed, formatMarker(options), options, eol);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Every marker pair in document order, shared with later calls for the same source.
|
|
48
|
-
* @param {string} source
|
|
49
|
-
* @returns {Marker[]}
|
|
50
|
-
*/
|
|
51
|
-
function findMarkers(source) {
|
|
52
|
-
return analyse(source).markers;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Every link to an anchor in the document, in order, shared with later calls for the same source.
|
|
57
|
-
* @param {string} source
|
|
58
|
-
* @returns {Anchor[]}
|
|
59
|
-
*/
|
|
60
|
-
function findAnchors(source) {
|
|
61
|
-
return analyse(source).anchors;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The GitHub anchor slug of a heading's rendered text.
|
|
66
|
-
* @param {string} text
|
|
67
|
-
* @returns {string}
|
|
68
|
-
*/
|
|
69
|
-
function slugify(text) {
|
|
70
|
-
return text
|
|
71
|
-
.toLowerCase()
|
|
72
|
-
.replace(/[^\p{L}\p{N}\p{M} _-]/gu, '')
|
|
73
|
-
.replace(/ /g, '-');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The text GitHub renders for a heading's inline markdown.
|
|
78
|
-
* @param {string} markdown
|
|
79
|
-
* @returns {string}
|
|
80
|
-
*/
|
|
81
|
-
function headingText(markdown) {
|
|
82
|
-
const { text, restore } = protect(stripClosingHashes(markdown));
|
|
83
|
-
const rendered = stripLinks(text)
|
|
84
|
-
.replace(/<\/?[a-zA-Z][^>]*>/g, '')
|
|
85
|
-
.replace(/~~(?=\S)(.+?)(?<=\S)~~/g, '$1')
|
|
86
|
-
.replace(/\*+(?=\S)|(?<=\S)\*+/g, '')
|
|
87
|
-
.replace(/(?<![\p{L}\p{N}_])_+(?=\S)|(?<=\S)_+(?![\p{L}\p{N}_])/gu, '');
|
|
88
|
-
return restore(rendered).trim();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The lines and the scan of the source.
|
|
93
|
-
* @param {string} source
|
|
94
|
-
* @returns {{ bom: string, lines: string[], eol: string, headlines: Headline[], markers: Marker[], anchors: Anchor[] }}
|
|
95
|
-
*/
|
|
96
|
-
function analyse(source) {
|
|
97
|
-
let result = cache.get(source);
|
|
98
|
-
if (!result) {
|
|
99
|
-
const { bom, lines, eol } = splitLines(source);
|
|
100
|
-
result = { bom, lines, eol, ...scan(lines) };
|
|
101
|
-
cache.clear();
|
|
102
|
-
cache.set(source, result);
|
|
103
|
-
}
|
|
104
|
-
return result;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The lines of the source, its byte order mark if any, and its line ending.
|
|
109
|
-
* @param {string} source
|
|
110
|
-
* @returns {{ bom: string, lines: string[], eol: string }}
|
|
111
|
-
*/
|
|
112
|
-
function splitLines(source) {
|
|
113
|
-
const bom = source.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
114
|
-
return { bom, lines: source.slice(bom.length).split(/\r?\n/), eol: source.includes('\r\n') ? '\r\n' : '\n' };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The headings, marker pairs and anchor links of the lines.
|
|
119
|
-
* @param {string[]} lines
|
|
120
|
-
* @returns {{ headlines: Headline[], markers: Marker[], anchors: Anchor[] }}
|
|
121
|
-
*/
|
|
122
|
-
function scan(lines) {
|
|
123
|
-
/** @type {Headline[]} */
|
|
124
|
-
const headlines = [];
|
|
125
|
-
/** @type {Marker[]} */
|
|
126
|
-
const markers = [];
|
|
127
|
-
/** @type {Array<{ line: number, text: string, anchor: string }>} */
|
|
128
|
-
const links = [];
|
|
129
|
-
/** @type {Set<string>} */
|
|
130
|
-
const ids = new Set();
|
|
131
|
-
/** @type {{ char: string, length: number } | null} */
|
|
132
|
-
let fence = null;
|
|
133
|
-
/** @type {Marker | null} */
|
|
134
|
-
let open = null;
|
|
135
|
-
let paragraph = false;
|
|
136
|
-
let comment = false;
|
|
137
|
-
|
|
138
|
-
for (const [i, line] of lines.entries()) {
|
|
139
|
-
const fenceMatch = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
140
|
-
if (fence) {
|
|
141
|
-
if (fenceMatch && fenceMatch[1][0] === fence.char && fenceMatch[1].length >= fence.length && !fenceMatch[2].trim()) {
|
|
142
|
-
fence = null;
|
|
143
|
-
}
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (fenceMatch && !(fenceMatch[1][0] === '`' && fenceMatch[2].includes('`'))) {
|
|
147
|
-
fence = { char: fenceMatch[1][0], length: fenceMatch[1].length };
|
|
148
|
-
paragraph = false;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (!paragraph && /^(?: {4}|\t)/.test(line)) continue;
|
|
152
|
-
paragraph = line.trim() !== '';
|
|
153
|
-
|
|
154
|
-
const { text: inline, restore } = protect(line);
|
|
155
|
-
let visible = inline;
|
|
156
|
-
if (comment) {
|
|
157
|
-
const close = visible.indexOf('-->');
|
|
158
|
-
if (close === -1) continue;
|
|
159
|
-
visible = visible.slice(close + 3);
|
|
160
|
-
comment = false;
|
|
161
|
-
}
|
|
162
|
-
visible = visible.replace(/<!--[\s\S]*?-->/g, '');
|
|
163
|
-
const start = visible.indexOf('<!--');
|
|
164
|
-
if (start !== -1) {
|
|
165
|
-
visible = visible.slice(0, start);
|
|
166
|
-
comment = true;
|
|
167
|
-
}
|
|
168
|
-
for (const [, tag, attributes] of visible.matchAll(/<([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*)>/g)) {
|
|
169
|
-
for (const [, name, quoted, single] of attributes.matchAll(/(?:^|\s)(id|name)=(?:"([^"]*)"|'([^']*)')/g)) {
|
|
170
|
-
if (name === 'id' || tag.toLowerCase() === 'a') ids.add(quoted ?? single);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
for (const [, text, anchor] of visible.matchAll(LINK)) links.push({ line: i, text: restore(text), anchor: restore(anchor) });
|
|
174
|
-
|
|
175
|
-
const startMatch = /^ {0,3}<!--\s*toc(?:\s+(.*?))?\s*-->\s*$/.exec(line);
|
|
176
|
-
if (startMatch) {
|
|
177
|
-
if (!open) markers.push((open = { start: i, end: -1, ...parseOptions(startMatch[1]) }));
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (/^ {0,3}<!--\s*\/toc\s*-->\s*$/.test(line)) {
|
|
181
|
-
if (open) open.end = i;
|
|
182
|
-
else markers.push({ start: -1, end: i, options: {} });
|
|
183
|
-
open = null;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const atx = /^ {0,3}(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
188
|
-
if (atx) {
|
|
189
|
-
headlines.push({ line: i, level: atx[1].length, markdown: atx[2], slug: '' });
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const setext = /^ {0,3}(=+|-+)\s*$/.exec(line);
|
|
194
|
-
if (setext && i > 0 && isParagraphText(lines[i - 1])) {
|
|
195
|
-
headlines.push({ line: i - 1, level: setext[1][0] === '=' ? 1 : 2, markdown: lines[i - 1].trim(), slug: '' });
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
assignSlugs(headlines);
|
|
199
|
-
return { headlines, markers, anchors: resolveAnchors(links, headlines, ids) };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* @param {Array<{ line: number, text: string, anchor: string }>} links
|
|
204
|
-
* @param {Headline[]} headlines
|
|
205
|
-
* @param {Set<string>} ids
|
|
206
|
-
* @returns {Anchor[]}
|
|
207
|
-
*/
|
|
208
|
-
function resolveAnchors(links, headlines, ids) {
|
|
209
|
-
const targets = new Set([...headlines.map((h) => h.slug), ...ids]);
|
|
210
|
-
return links.map(({ line, text, anchor }) => {
|
|
211
|
-
const decoded = decodeAnchor(anchor);
|
|
212
|
-
if (targets.has(anchor) || targets.has(decoded)) return { line, text, anchor, valid: true };
|
|
213
|
-
const candidates = new Set([slugify(headingText(decoded)), slugify(headingText(text))].filter((c) => targets.has(c)));
|
|
214
|
-
if (candidates.size !== 1) return { line, text, anchor, valid: false };
|
|
215
|
-
return { line, text, anchor, valid: false, suggestion: [...candidates][0] };
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** @param {string} anchor */
|
|
220
|
-
function decodeAnchor(anchor) {
|
|
221
|
-
try {
|
|
222
|
-
return decodeURIComponent(anchor);
|
|
223
|
-
} catch {
|
|
224
|
-
return anchor;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Set the slug of every heading.
|
|
230
|
-
* @param {Headline[]} headlines
|
|
231
|
-
*/
|
|
232
|
-
function assignSlugs(headlines) {
|
|
233
|
-
/** @type {Record<string, number>} */
|
|
234
|
-
const occurrences = {};
|
|
235
|
-
for (const headline of headlines) {
|
|
236
|
-
const base = slugify(headingText(headline.markdown));
|
|
237
|
-
let slug = base;
|
|
238
|
-
while (Object.hasOwn(occurrences, slug)) {
|
|
239
|
-
occurrences[base]++;
|
|
240
|
-
slug = `${base}-${occurrences[base]}`;
|
|
241
|
-
}
|
|
242
|
-
occurrences[slug] = 0;
|
|
243
|
-
headline.slug = slug;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* The toc block for the headings, between the start marker line and the end marker.
|
|
249
|
-
* @param {Headline[]} headlines
|
|
250
|
-
* @param {string} startLine
|
|
251
|
-
* @param {TocOptions} options
|
|
252
|
-
* @param {string} [eol]
|
|
253
|
-
*/
|
|
254
|
-
function renderBlock(headlines, startLine, options, eol = '\n') {
|
|
255
|
-
let minLevel = Infinity;
|
|
256
|
-
const tocLines = headlines.map(({ level, markdown, slug }) => {
|
|
257
|
-
minLevel = Math.min(minLevel, level);
|
|
258
|
-
return `${' '.repeat(level - minLevel)}- [${headingLabel(markdown)}](#${slug})`;
|
|
259
|
-
});
|
|
260
|
-
const details = options.collapsible ?? options.collapsed;
|
|
261
|
-
if (details === undefined) return [startLine, '', ...tocLines, '', TOC_END].join(eol);
|
|
262
|
-
const summary = details === true ? DEFAULT_SUMMARY : details;
|
|
263
|
-
const tag = options.collapsible === undefined ? '<details>' : '<details open>';
|
|
264
|
-
const attrs = formatAttributes(options.attributes ?? {})
|
|
265
|
-
.map((a) => ` ${a}`)
|
|
266
|
-
.join('');
|
|
267
|
-
return [startLine, tag, `<summary${attrs}>${summary}</summary>`, '', ...tocLines, '', '</details>', TOC_END].join(eol);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* The options written on a start marker, with a problem when they cannot be used.
|
|
272
|
-
* @param {string | undefined} text
|
|
273
|
-
* @returns {{ options: TocOptions, problem?: string }}
|
|
274
|
-
*/
|
|
275
|
-
function parseOptions(text) {
|
|
276
|
-
/** @type {TocOptions} */
|
|
277
|
-
const options = {};
|
|
278
|
-
/** @type {Record<string, string>} */
|
|
279
|
-
const attributes = {};
|
|
280
|
-
/** @type {string[]} */
|
|
281
|
-
const unknown = [];
|
|
282
|
-
for (const [token, name, value] of (text ?? '').matchAll(/([^\s="]+)(?:="([^"]*)")?(?=\s|$)|\S+/g)) {
|
|
283
|
-
if (name && KNOWN_OPTIONS.includes(name)) options[/** @type {'collapsible' | 'collapsed'} */ (name)] = value ?? true;
|
|
284
|
-
else if (name && value !== undefined && /^[a-zA-Z][\w:.-]*$/.test(name)) attributes[name] = value;
|
|
285
|
-
else unknown.push(token);
|
|
286
|
-
}
|
|
287
|
-
const names = Object.keys(attributes);
|
|
288
|
-
if (names.length) options.attributes = attributes;
|
|
289
|
-
if (unknown.length) return { options, problem: `unknown TOC option ${unknown.join(' ')}` };
|
|
290
|
-
if (options.collapsible !== undefined && options.collapsed !== undefined) {
|
|
291
|
-
return { options, problem: 'TOC options collapsible and collapsed exclude each other' };
|
|
292
|
-
}
|
|
293
|
-
if (names.length && options.collapsible === undefined && options.collapsed === undefined) {
|
|
294
|
-
return { options, problem: `TOC attributes ${names.join(', ')} need collapsible or collapsed` };
|
|
295
|
-
}
|
|
296
|
-
return { options };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* The start marker line for the options.
|
|
301
|
-
* @param {TocOptions} options
|
|
302
|
-
*/
|
|
303
|
-
function formatMarker(options) {
|
|
304
|
-
const { attributes = {}, ...named } = options;
|
|
305
|
-
const parts = [
|
|
306
|
-
...Object.entries(named).map(([name, value]) => (value === true ? name : `${name}="${value}"`)),
|
|
307
|
-
...formatAttributes(attributes),
|
|
308
|
-
];
|
|
309
|
-
return parts.length ? `<!-- toc ${parts.join(' ')} -->` : TOC_START;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* @param {Record<string, string>} attributes
|
|
314
|
-
* @returns {string[]}
|
|
315
|
-
*/
|
|
316
|
-
function formatAttributes(attributes) {
|
|
317
|
-
return Object.entries(attributes).map(([name, value]) => `${name}="${value}"`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* The link text used in the toc for a heading.
|
|
322
|
-
* @param {string} markdown
|
|
323
|
-
* @returns {string}
|
|
324
|
-
*/
|
|
325
|
-
function headingLabel(markdown) {
|
|
326
|
-
const { text, restore } = protect(stripClosingHashes(markdown), { keepCodeSpans: true });
|
|
327
|
-
return restore(stripLinks(text)).trim();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* The markdown with code spans and backslash escapes swapped for placeholders, and a function to put them back.
|
|
332
|
-
* @param {string} markdown
|
|
333
|
-
* @param {{ keepCodeSpans?: boolean }} [options]
|
|
334
|
-
*/
|
|
335
|
-
function protect(markdown, { keepCodeSpans = false } = {}) {
|
|
336
|
-
/** @type {string[]} */
|
|
337
|
-
const kept = [];
|
|
338
|
-
/** @param {string} value */
|
|
339
|
-
const keep = (value) => `${NUL}${kept.push(value) - 1}${NUL}`;
|
|
340
|
-
const text = markdown
|
|
341
|
-
.replace(/(`+)(.+?)\1(?!`)/g, (span, _ticks, code) => keep(keepCodeSpans ? span : unpadCodeSpan(code)))
|
|
342
|
-
.replace(/\\([!-/:-@[-`{-~])/g, (_, char) => keep(char));
|
|
343
|
-
return {
|
|
344
|
-
text,
|
|
345
|
-
/** @param {string} value */
|
|
346
|
-
restore: (value) => value.replace(new RegExp(`${NUL}(\\d+)${NUL}`, 'g'), (_, i) => kept[Number(i)]),
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* The code span content unpadded as CommonMark renders it.
|
|
352
|
-
* @param {string} code
|
|
353
|
-
*/
|
|
354
|
-
function unpadCodeSpan(code) {
|
|
355
|
-
if (code.length > 2 && code.startsWith(' ') && code.endsWith(' ') && code.trim() !== '') return code.slice(1, -1);
|
|
356
|
-
return code;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** @param {string} markdown */
|
|
360
|
-
function stripClosingHashes(markdown) {
|
|
361
|
-
return markdown.replace(/(^|\s+)#+\s*$/, '');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* The text with links, images and autolinks flattened to their text or url.
|
|
366
|
-
* @param {string} text
|
|
367
|
-
*/
|
|
368
|
-
function stripLinks(text) {
|
|
369
|
-
return text
|
|
370
|
-
.replace(/<(https?:\/\/[^>\s]+|mailto:[^>\s]+)>/g, '$1')
|
|
371
|
-
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
372
|
-
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
373
|
-
.replace(/\[([^\]]*)\]\[[^\]]*\]/g, '$1');
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** @param {string} line */
|
|
377
|
-
function isParagraphText(line) {
|
|
378
|
-
const trimmed = line.trim();
|
|
379
|
-
return trimmed !== '' && !/^(#|>|[-*+]\s|\d+[.)]\s|\||[-=]+\s*$|```|~~~|<!--)/.test(trimmed);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Options written on a start marker, each `true` or a summary text, and the other attributes for the summary element.
|
|
384
|
-
* @typedef {{ collapsible?: true | string, collapsed?: true | string, attributes?: Record<string, string> }} TocOptions
|
|
385
|
-
*/
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* A heading with its zero based line, level, inline markdown and GitHub slug.
|
|
389
|
-
* @typedef {{ line: number, level: number, markdown: string, slug: string }} Headline
|
|
390
|
-
*/
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* A link to an anchor with its zero based line, text, anchor as written, whether it has a target and, when it
|
|
394
|
-
* has none and one heading clearly matches, a suggestion.
|
|
395
|
-
* @typedef {{ line: number, text: string, anchor: string, valid: boolean, suggestion?: string }} Anchor
|
|
396
|
-
*/
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* A marker pair as zero based lines, -1 for a missing side, with its options and a problem when they cannot be used.
|
|
400
|
-
* @typedef {{ start: number, end: number, options: TocOptions, problem?: string }} Marker
|
|
401
|
-
*/
|
|
402
|
-
|
|
403
|
-
exports.TOC_END = TOC_END;
|
|
404
|
-
exports.TOC_START = TOC_START;
|
|
405
|
-
exports.buildToc = buildToc;
|
|
406
|
-
exports.findAnchors = findAnchors;
|
|
407
|
-
exports.findMarkers = findMarkers;
|
|
408
|
-
exports.headingText = headingText;
|
|
409
|
-
exports.renderToc = renderToc;
|
|
410
|
-
exports.slugify = slugify;
|