remark-flexible-toc 1.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/LICENSE +21 -0
- package/README.md +413 -0
- package/dist/esm/index.d.ts +38 -0
- package/dist/esm/index.js +111 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +76 -0
- package/src/index.ts +192 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 ipikuka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# remark-flexible-toc
|
|
2
|
+
|
|
3
|
+
[![NPM version][npm-image]][npm-url]
|
|
4
|
+
[![Build][github-build]][github-build-url]
|
|
5
|
+
![npm-typescript]
|
|
6
|
+
[![License][github-license]][github-license-url]
|
|
7
|
+
|
|
8
|
+
This package is a [unified][unified] ([remark][remark]) plugin to expose the table of contents via Vfile.data or via an option reference (compatible with new parser "[micromark][micromark]").
|
|
9
|
+
|
|
10
|
+
"**unified**" is a project that transforms content with abstract syntax trees (ASTs). "**remark**" adds support for markdown to unified. "**mdast**" is the markdown abstract syntax tree (AST) that remark uses.
|
|
11
|
+
|
|
12
|
+
**This plugin is a remark plugin that gets info from the mdast.**
|
|
13
|
+
|
|
14
|
+
## When should I use this?
|
|
15
|
+
|
|
16
|
+
This plugin `remark-flexible-toc` is useful if you want to get the table of contents (TOC) from the markdown/MDX document. The `remark-flexible-toc` exposes the table of contents (TOC) in two ways:
|
|
17
|
+
+ by adding the `toc` into the Vfile.data
|
|
18
|
+
+ by mutating an array of reference if provided in the options
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
This package is suitable for ESM only. In Node.js (version 16+), install with npm:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install remark-flexible-toc
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
or
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
yarn add remark-flexible-toc
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Say we have the following file, `example.md`, which consists some headings.
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
# The Main Heading
|
|
40
|
+
|
|
41
|
+
## Section
|
|
42
|
+
|
|
43
|
+
### Subheading 1
|
|
44
|
+
|
|
45
|
+
### Subheading 2
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
And our module, `example.js`, looks as follows:
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
import { read } from "to-vfile";
|
|
52
|
+
import remark from "remark";
|
|
53
|
+
import gfm from "remark-gfm";
|
|
54
|
+
import remarkRehype from "remark-rehype";
|
|
55
|
+
import rehypeStringify from "rehype-stringify";
|
|
56
|
+
import remarkFlexibleToc from "remark-flexible-toc";
|
|
57
|
+
|
|
58
|
+
main();
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const toc = [];
|
|
62
|
+
|
|
63
|
+
const file = await remark()
|
|
64
|
+
.use(gfm)
|
|
65
|
+
.use(remarkFlexibleToc, {tocRef: toc})
|
|
66
|
+
.use(remarkRehype)
|
|
67
|
+
.use(rehypeStringify)
|
|
68
|
+
.process(await read("example.md"));
|
|
69
|
+
|
|
70
|
+
// the first way of getting the table of contents (TOC) via file.data
|
|
71
|
+
console.log(file.data.toc);
|
|
72
|
+
|
|
73
|
+
// the second way of getting the table of contents (TOC), since we provided an array of reference in the options
|
|
74
|
+
console.log(toc);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Now, running `node example.js` you see that the same table of contents is logged in the console:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
depth: 2,
|
|
84
|
+
href: "#section",
|
|
85
|
+
numbering: [1, 1],
|
|
86
|
+
parent: "root",
|
|
87
|
+
value: "Section",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
depth: 3,
|
|
91
|
+
href: "#subheading-1",
|
|
92
|
+
numbering: [1, 1, 1],
|
|
93
|
+
parent: "root",
|
|
94
|
+
value: "Subheading 1",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
depth: 3,
|
|
98
|
+
href: "#subheading-2",
|
|
99
|
+
numbering: [1, 1, 2],
|
|
100
|
+
parent: "root",
|
|
101
|
+
value: "Subheading 2",
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Without `remark-flexible-toc`, there wouldn't be any `toc` key in the `file.data`:
|
|
107
|
+
|
|
108
|
+
## Options
|
|
109
|
+
|
|
110
|
+
All options are **optional**.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
type HeadingParent =
|
|
114
|
+
| "root"
|
|
115
|
+
| "blockquote"
|
|
116
|
+
| "footnoteDefinition"
|
|
117
|
+
| "listItem"
|
|
118
|
+
| "container"
|
|
119
|
+
| "mdxJsxFlowElement";
|
|
120
|
+
|
|
121
|
+
type HeadingDepth = 1 | 2 | 3 | 4 | 5 | 6;
|
|
122
|
+
|
|
123
|
+
use(remarkFlexibleToc, {
|
|
124
|
+
tocName?: string; // default: "toc"
|
|
125
|
+
tocRef?: TocItem[]; // default: []
|
|
126
|
+
maxDepth?: HeadingDepth; // default: 6
|
|
127
|
+
skipLevels?: HeadingDepth[]; // default: [1]
|
|
128
|
+
skipParents?: Exclude<HeadingParent, "root">[]; // default: []
|
|
129
|
+
exclude?: string | string[]; // default is undefined
|
|
130
|
+
prefix?: string; // default is undefined
|
|
131
|
+
fallback?: (toc: TocItem[]) => undefined; // default is undefined
|
|
132
|
+
} as FlexibleTocOptions);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### `tocName`
|
|
136
|
+
|
|
137
|
+
It is a **string** option in which the table of contents (TOC) is placed in the `vfile.data`.
|
|
138
|
+
|
|
139
|
+
By default it is **`toc`**, meaningly the TOC is reachable via `vfile.data.toc`.
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
use(remarkFlexibleToc, {
|
|
143
|
+
tocName: "headings";
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
Now, the TOC is accessable via `vfile.data.headings`.
|
|
147
|
+
|
|
148
|
+
#### `tocRef`
|
|
149
|
+
|
|
150
|
+
It is an **array of reference** option for getting the table of contents (TOC), which is the second way of getting the TOC from the `remark-flexible-toc`.
|
|
151
|
+
|
|
152
|
+
The reference array should be an empty array, if not, it is emptied by the plugin.
|
|
153
|
+
|
|
154
|
+
If you use _typescript_, the array reference should be `const toc: TocItem[] = []`.
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const toc = [];
|
|
158
|
+
|
|
159
|
+
use(remarkFlexibleToc, {
|
|
160
|
+
tocRef: toc; // the `remark-flexible-toc` mutates the array of reference
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Now, the TOC is accessable via `toc`.
|
|
165
|
+
|
|
166
|
+
#### `maxDepth`
|
|
167
|
+
|
|
168
|
+
It is a **number** option for indicating the max heading depth to include in the TOC.
|
|
169
|
+
|
|
170
|
+
By default it is `6`. Meaningly, there is no restriction by default.
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
use(remarkFlexibleToc, {
|
|
174
|
+
maxDepth: 4;
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The `maxDepth` option is inclusive: when set to 4, level fourth headings are included, but fifth and sixth level headings will be skipped.
|
|
179
|
+
|
|
180
|
+
#### `skipLevels`
|
|
181
|
+
|
|
182
|
+
It is an **array** option to indicate the heading levels to be skipped.
|
|
183
|
+
|
|
184
|
+
By default it is `[1]` since the first level heading is not expected to be in the TOC.
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
use(remarkFlexibleToc, {
|
|
188
|
+
skipLevels: [1, 2, 4, 5, 6];
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Now, the TOC consists only the third level headings.
|
|
193
|
+
|
|
194
|
+
#### `skipParents`
|
|
195
|
+
|
|
196
|
+
By default it is an empty array `[]`. The array may contain the parent values `blockquote`, `footnoteDefinition`, `listItem`, `container`, `mdxJsxFlowElement`.
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
use(remarkFlexibleToc, {
|
|
200
|
+
skipParents: ["blockquote"];
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Now, the headings in the `<blockquote>` will not be added into the TOC.
|
|
205
|
+
|
|
206
|
+
#### `exclude`
|
|
207
|
+
|
|
208
|
+
It is a **string** or **string[]** option. The plugin wraps the string(s) in new RegExp('^(' + value + ')$', 'i'), so any heading matching this expression will not be present in the TOC. The RegExp checks exact (not contain) matching and case insensitive as you see.
|
|
209
|
+
|
|
210
|
+
The option has no default value.
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
use(remarkFlexibleToc, {
|
|
214
|
+
exclude: "The Subheading";
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
Now, the heading "The Subheading" will not be included in to the TOC, but forexample "The Subheading Something" will be included.
|
|
218
|
+
|
|
219
|
+
#### `prefix`
|
|
220
|
+
|
|
221
|
+
It is a **string** option to add a prefix to `href`s of the TOC items. It is useful for example when later going from markdown to HTML and sanitizing with `rehype-sanitize`.
|
|
222
|
+
|
|
223
|
+
The option has no default value.
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
use(remarkFlexibleToc, {
|
|
227
|
+
prefix: "text-prefix-";
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
Now, all TOC items' `href`s will start with that prefix like `#text-prefix-the-subheading`.
|
|
231
|
+
|
|
232
|
+
#### `callback`
|
|
233
|
+
|
|
234
|
+
It is a **callback function** `callback?: (toc: TocItem[]) => undefined;` which takes the TOC items as an argument and returns nothing. It is usefull for logging the TOC, forexample, or modifing the TOC. **It is allowed that the callback function is able mutate the TOC items !.**
|
|
235
|
+
|
|
236
|
+
The option has no default value.
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
use(remarkFlexibleToc, {
|
|
240
|
+
callback: (toc) => {
|
|
241
|
+
console.log(toc);
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Now, each time when you compile the source, the TOC will be logged into console for debugging purpose.
|
|
247
|
+
|
|
248
|
+
## A Table of Content (TOC) Item
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
type TocItem = {
|
|
252
|
+
value: string; // heading text
|
|
253
|
+
href: string; // produced uniquely by "github-slugger" using the value of the heading
|
|
254
|
+
depth: HeadingDepth; // 1 | 2 | 3 | 4 | 5 | 6
|
|
255
|
+
numbering: number[]; // explained below
|
|
256
|
+
parent: HeadingParent; // "root"| "blockquote" | "footnoteDefinition" | "listItem" | "container" | "mdxJsxFlowElement"
|
|
257
|
+
data?: Record<string, unknown>; // Other remark plugins can store custom data in "node.data.hProperties" like "id" etc.
|
|
258
|
+
};
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
As a note, the `remark-flexible-toc` uses the `github-slugger` internally for producing unique links. Then, it is possible you to use [`rehype-slug`](https://github.com/rehypejs/rehype-slug) (forIDs on headings) and [`rehype-autolink-headings`](https://github.com/rehypejs/rehype-autolink-headings) (for anchors that link-to-self) because they use the same `github-slugger`.
|
|
262
|
+
|
|
263
|
+
As an example for the unique heading links (notice the same heading texts).
|
|
264
|
+
|
|
265
|
+
```markdown
|
|
266
|
+
# The Main Heading
|
|
267
|
+
|
|
268
|
+
## Section
|
|
269
|
+
|
|
270
|
+
### Subheading
|
|
271
|
+
|
|
272
|
+
## Section
|
|
273
|
+
|
|
274
|
+
### Subheading
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The `github-slugger` produces unique links with using a counter mechanism internally, and the TOC item's `href`s is going to be unique.
|
|
278
|
+
|
|
279
|
+
```javascript
|
|
280
|
+
[
|
|
281
|
+
{
|
|
282
|
+
depth: 2,
|
|
283
|
+
href: "#section",
|
|
284
|
+
numbering: [1, 1],
|
|
285
|
+
parent: "root",
|
|
286
|
+
value: "Section",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
depth: 3,
|
|
290
|
+
href: "#subheading",
|
|
291
|
+
numbering: [1, 1, 1],
|
|
292
|
+
parent: "root",
|
|
293
|
+
value: "Subheading",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
depth: 2,
|
|
297
|
+
href: "#section-1",
|
|
298
|
+
numbering: [1, 2],
|
|
299
|
+
parent: "root",
|
|
300
|
+
value: "Section",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
depth: 3,
|
|
304
|
+
href: "#subheading-1",
|
|
305
|
+
numbering: [1, 2, 1],
|
|
306
|
+
parent: "root",
|
|
307
|
+
value: "Subheading",
|
|
308
|
+
},
|
|
309
|
+
]
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Numbering for Ordered Table of Contents
|
|
313
|
+
|
|
314
|
+
The `remark-flexible-toc` produces always the `numbering` for the TOC items in case you show the ordered TOC.
|
|
315
|
+
|
|
316
|
+
The **numbering** of a TOC item is an array of number. The numbers in the `numbering` corresponds the **level of the headers**. With that structure, you know which header is under which header.
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
[1, 1]
|
|
320
|
+
[1, 2]
|
|
321
|
+
[1, 2, 1]
|
|
322
|
+
[1, 2, 2]
|
|
323
|
+
[1, 3]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The first number of the `numbering` is related with the fist level headings.
|
|
327
|
+
The second number of the `numbering` is related with the second level headings.
|
|
328
|
+
And so on...
|
|
329
|
+
|
|
330
|
+
If yo haven't included the first level header into the TOC, you can slice the `numbering` with `1` so as to second level headings starts with `1` and so on..
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
tocItem.numbering.slice(1);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
You can join the `numbering` as you wish. It is up to you to combine the `numbering` with dot, or dash.
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
tocItem.numbering.join(".");
|
|
340
|
+
tocItem.numbering.join("-");
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Syntax tree
|
|
344
|
+
|
|
345
|
+
This plugin does not modify the `mdast` (markdown abstract syntax tree), collects data from the `mdast` and adds information into the `vfile.data` if required.
|
|
346
|
+
|
|
347
|
+
## Types
|
|
348
|
+
|
|
349
|
+
This package is fully typed with [TypeScript][typeScript].
|
|
350
|
+
|
|
351
|
+
The plugin exports the types `FlexibleTocOptions`, `HeadingParent`, `HeadingDepth`, `TocItem`.
|
|
352
|
+
|
|
353
|
+
## Compatibility
|
|
354
|
+
|
|
355
|
+
This plugin works with unified version 6+ and remark version 7+. It is compatible with MDX version.3.
|
|
356
|
+
|
|
357
|
+
## Security
|
|
358
|
+
|
|
359
|
+
Use of `remark-flexible-toc` does not involve rehype (hast) or user content so there are no openings for cross-site scripting (XSS) attacks.
|
|
360
|
+
|
|
361
|
+
## My Plugins
|
|
362
|
+
|
|
363
|
+
### My Remark Plugins
|
|
364
|
+
|
|
365
|
+
+ [`remark-flexible-code-titles`](https://www.npmjs.com/package/remark-flexible-code-titles)
|
|
366
|
+
– Remark plugin to add titles or/and containers for the code blocks with customizable properties
|
|
367
|
+
+ [`remark-flexible-containers`](https://www.npmjs.com/package/remark-flexible-containers)
|
|
368
|
+
– Remark plugin to add custom containers with customizable properties in markdown
|
|
369
|
+
+ [`remark-ins`](https://www.npmjs.com/package/remark-ins)
|
|
370
|
+
– Remark plugin to add `ins` element in markdown
|
|
371
|
+
+ [`remark-flexible-paragraphs`](https://www.npmjs.com/package/remark-flexible-paragraphs)
|
|
372
|
+
– Remark plugin to add custom paragraphs with customizable properties in markdown
|
|
373
|
+
+ [`remark-flexible-markers`](https://www.npmjs.com/package/remark-flexible-markers)
|
|
374
|
+
– Remark plugin to add custom `mark` element with customizable properties in markdown
|
|
375
|
+
+ [`remark-flexible-toc`](https://www.npmjs.com/package/remark-flexible-toc)
|
|
376
|
+
– Remark plugin to expose the table of contents via Vfile.data or via an option reference
|
|
377
|
+
|
|
378
|
+
### My Recma Plugins
|
|
379
|
+
|
|
380
|
+
+ [`recma-mdx-escape-missing-components`](https://www.npmjs.com/package/recma-mdx-escape-missing-components)
|
|
381
|
+
– Recma plugin to to set the default value `() => null` for the Components in MDX in case of missing or not provided
|
|
382
|
+
+ [`recma-mdx-change-props`](https://www.npmjs.com/package/recma-mdx-change-props)
|
|
383
|
+
– Recma plugin to change the 'props' parameter into '_props' in the function '_createMdxContent' in the compiled source in order to be able to use {props.foo} like expressions for the `next-mdx-remote` or `next-mdx-remote-client` users.
|
|
384
|
+
|
|
385
|
+
## License
|
|
386
|
+
|
|
387
|
+
[MIT][license] © ipikuka
|
|
388
|
+
|
|
389
|
+
### Keywords
|
|
390
|
+
|
|
391
|
+
[unified][unifiednpm] [remark][remarknpm] [remark-plugin][remarkpluginnpm] [mdast][mdastnpm] [markdown][markdownnpm] [mdxnpm][mdxnpm] [remark toc][remarktocnpm] [remark table of contents][remarktableofcontentsnpm]
|
|
392
|
+
|
|
393
|
+
[unified]: https://github.com/unifiedjs/unified
|
|
394
|
+
[unifiednpm]: https://www.npmjs.com/search?q=keywords:unified
|
|
395
|
+
[remark]: https://github.com/remarkjs/remark
|
|
396
|
+
[remarknpm]: https://www.npmjs.com/search?q=keywords:remark
|
|
397
|
+
[remarkpluginnpm]: https://www.npmjs.com/search?q=keywords:remark%20plugin
|
|
398
|
+
[mdast]: https://github.com/syntax-tree/mdast
|
|
399
|
+
[mdastnpm]: https://www.npmjs.com/search?q=keywords:mdast
|
|
400
|
+
[micromark]: https://github.com/micromark/micromark
|
|
401
|
+
[typescript]: https://www.typescriptlang.org/
|
|
402
|
+
[license]: https://github.com/ipikuka/remark-flexible-toc/blob/main/LICENSE
|
|
403
|
+
[mdxnpm]: https://www.npmjs.com/search?q=keywords:mdx
|
|
404
|
+
[markdownnpm]: https://www.npmjs.com/search?q=keywords:markdown
|
|
405
|
+
[remarktocnpm]: https://www.npmjs.com/search?q=keywords:remark%20toc
|
|
406
|
+
[remarktableofcontentsnpm]: https://www.npmjs.com/search?q=keywords:remark%20table%20of%20contents
|
|
407
|
+
[npm-url]: https://www.npmjs.com/package/remark-flexible-toc
|
|
408
|
+
[npm-image]: https://img.shields.io/npm/v/remark-flexible-toc
|
|
409
|
+
[github-license]: https://img.shields.io/github/license/ipikuka/remark-flexible-toc
|
|
410
|
+
[github-license-url]: https://github.com/ipikuka/remark-flexible-toc/blob/master/LICENSE
|
|
411
|
+
[github-build]: https://github.com/ipikuka/remark-flexible-toc/actions/workflows/publish.yml/badge.svg
|
|
412
|
+
[github-build-url]: https://github.com/ipikuka/remark-flexible-toc/actions/workflows/publish.yml
|
|
413
|
+
[npm-typescript]: https://img.shields.io/npm/types/remark-flexible-toc
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Plugin } from "unified";
|
|
2
|
+
import { type Root } from "mdast";
|
|
3
|
+
export type Prettify<T> = {
|
|
4
|
+
[K in keyof T]: T[K];
|
|
5
|
+
} & {};
|
|
6
|
+
export type PartiallyRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
7
|
+
export type HeadingParent = "root" | "blockquote" | "footnoteDefinition" | "listItem" | "container" | "mdxJsxFlowElement";
|
|
8
|
+
export type HeadingDepth = 1 | 2 | 3 | 4 | 5 | 6;
|
|
9
|
+
export type TocItem = {
|
|
10
|
+
value: string;
|
|
11
|
+
href: string;
|
|
12
|
+
depth: HeadingDepth;
|
|
13
|
+
numbering: number[];
|
|
14
|
+
parent: HeadingParent;
|
|
15
|
+
data?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* tocName (default: "toc") - the key name which is attached into vfile.data
|
|
19
|
+
* tocRef (default: []) — another way of exposing the tocItems
|
|
20
|
+
* maxDepth (default: 6) — max heading depth to include in the table of contents; this is inclusive: when set to 3, level three headings are included
|
|
21
|
+
* skipLevels (default: [1]) — disallowed heading levels, by default the article h1 is not expected to be in the TOC
|
|
22
|
+
* skipParents (default: []) — disallow headings to be children of certain node types, (the "root" can not be skipped)
|
|
23
|
+
* exclude — headings to skip, wrapped in new RegExp('^(' + value + ')$', 'i'); any heading matching this expression will not be present in the table of contents
|
|
24
|
+
* prefix - the text that will be attached to headings as prefix, like "text-prefix-"
|
|
25
|
+
* callback - It is a callback function to take the array of toc items as an argument
|
|
26
|
+
*/
|
|
27
|
+
export type FlexibleTocOptions = {
|
|
28
|
+
tocName?: string;
|
|
29
|
+
tocRef?: TocItem[];
|
|
30
|
+
maxDepth?: HeadingDepth;
|
|
31
|
+
skipLevels?: HeadingDepth[];
|
|
32
|
+
skipParents?: Exclude<HeadingParent, "root">[];
|
|
33
|
+
exclude?: string | string[];
|
|
34
|
+
prefix?: string;
|
|
35
|
+
callback?: (toc: TocItem[]) => undefined;
|
|
36
|
+
};
|
|
37
|
+
declare const RemarkFlexibleToc: Plugin<[FlexibleTocOptions?], Root>;
|
|
38
|
+
export default RemarkFlexibleToc;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { visit, CONTINUE } from "unist-util-visit";
|
|
2
|
+
import GithubSlugger from "github-slugger";
|
|
3
|
+
import { toString } from "mdast-util-to-string";
|
|
4
|
+
const DEFAULT_SETTINGS = {
|
|
5
|
+
tocName: "toc",
|
|
6
|
+
tocRef: [],
|
|
7
|
+
maxDepth: 6,
|
|
8
|
+
skipLevels: [1],
|
|
9
|
+
skipParents: [],
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* adds numberings to the TOC items.
|
|
13
|
+
* why "number[]"? It is because up to you joining with dot or dash or slicing the first number (reserved for h1)
|
|
14
|
+
*
|
|
15
|
+
* [1]
|
|
16
|
+
* [1,1]
|
|
17
|
+
* [1,2]
|
|
18
|
+
* [1,2,1]
|
|
19
|
+
*/
|
|
20
|
+
function addNumbering(arr) {
|
|
21
|
+
for (let i = 0; i < arr.length; i++) {
|
|
22
|
+
const tocItem = arr[i];
|
|
23
|
+
const depth = tocItem.depth;
|
|
24
|
+
let numbering = [];
|
|
25
|
+
const prevObj = i > 0 ? arr[i - 1] : undefined;
|
|
26
|
+
const prevDepth = prevObj ? prevObj.depth : undefined;
|
|
27
|
+
const prevNumbering = prevObj ? prevObj.numbering : undefined;
|
|
28
|
+
if (!prevNumbering || !prevDepth) {
|
|
29
|
+
numbering = Array.from({ length: depth }, () => 1);
|
|
30
|
+
}
|
|
31
|
+
else if (depth === prevDepth) {
|
|
32
|
+
numbering = [...prevNumbering];
|
|
33
|
+
numbering[depth - 1]++;
|
|
34
|
+
}
|
|
35
|
+
else if (depth > prevDepth) {
|
|
36
|
+
numbering = [
|
|
37
|
+
...prevNumbering,
|
|
38
|
+
...Array.from({ length: depth - prevDepth }, // if depth is more bigger than prevDepth, put more "1" inside the array
|
|
39
|
+
() => 1),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
else if (depth < prevDepth) {
|
|
43
|
+
numbering = prevNumbering.slice(0, depth);
|
|
44
|
+
numbering[depth - 1]++;
|
|
45
|
+
}
|
|
46
|
+
tocItem.numbering = numbering;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const RemarkFlexibleToc = (options) => {
|
|
50
|
+
const settings = Object.assign({}, DEFAULT_SETTINGS, options);
|
|
51
|
+
const exludeRegexFilter = settings.exclude &&
|
|
52
|
+
(Array.isArray(settings.exclude)
|
|
53
|
+
? new RegExp("^(" + settings.exclude.join("|") + ")$", "i")
|
|
54
|
+
: new RegExp("^(" + settings.exclude + ")$", "i"));
|
|
55
|
+
return (tree, file) => {
|
|
56
|
+
const slugger = new GithubSlugger();
|
|
57
|
+
const tocItems = [];
|
|
58
|
+
visit(tree, "heading", (_node, _index, _parent) => {
|
|
59
|
+
if (!_parent)
|
|
60
|
+
return;
|
|
61
|
+
const depth = _node.depth;
|
|
62
|
+
const value = toString(_node, { includeImageAlt: false });
|
|
63
|
+
const href = `#${settings.prefix ?? ""}${slugger.slug(value)}`;
|
|
64
|
+
const parent = _parent.type;
|
|
65
|
+
// maxDepth check
|
|
66
|
+
if (depth > settings.maxDepth)
|
|
67
|
+
return CONTINUE;
|
|
68
|
+
// skipLevels check
|
|
69
|
+
if (settings.skipLevels.includes(depth))
|
|
70
|
+
return CONTINUE;
|
|
71
|
+
// skipParents check
|
|
72
|
+
if (parent !== "root" && settings.skipParents.includes(parent))
|
|
73
|
+
return CONTINUE;
|
|
74
|
+
// exclude check
|
|
75
|
+
if (exludeRegexFilter && exludeRegexFilter.test(value))
|
|
76
|
+
return CONTINUE;
|
|
77
|
+
// Other remark plugins can store custom data in node.data.hProperties
|
|
78
|
+
// I omitted node.data.hName and node.data.hChildren since not related with toc
|
|
79
|
+
const data = _node.data?.hProperties
|
|
80
|
+
? { ..._node.data.hProperties }
|
|
81
|
+
: undefined;
|
|
82
|
+
tocItems.push({
|
|
83
|
+
value,
|
|
84
|
+
href,
|
|
85
|
+
depth,
|
|
86
|
+
numbering: [],
|
|
87
|
+
parent,
|
|
88
|
+
...(data && { data }),
|
|
89
|
+
});
|
|
90
|
+
return CONTINUE;
|
|
91
|
+
});
|
|
92
|
+
addNumbering(tocItems);
|
|
93
|
+
// it is allowed to modify the TOC in the callback
|
|
94
|
+
settings.callback?.(tocItems);
|
|
95
|
+
// method - 1 for exposing the data via vfile.data **************************
|
|
96
|
+
// other plugins are not allowed to mutate the exposed TOC
|
|
97
|
+
// The spreading is slower than push but need to fresh copy
|
|
98
|
+
file.data[settings.tocName] = [...tocItems];
|
|
99
|
+
// method - 2 for exposing the data via reference array *********************
|
|
100
|
+
if (options?.tocRef) {
|
|
101
|
+
// prevent dublication if the plugin is called more than once
|
|
102
|
+
settings.tocRef.length = 0;
|
|
103
|
+
tocItems.forEach((tocItem) => {
|
|
104
|
+
// the tocRef is not allowed to mutate the vfile.data.toc
|
|
105
|
+
settings.tocRef.push(tocItem);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
export default RemarkFlexibleToc;
|
|
111
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,aAAa,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAuDhD,MAAM,gBAAgB,GAAG;IACvB,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,EAAE;IACV,QAAQ,EAAE,CAAC;IACX,UAAU,EAAE,CAAC,CAAC,CAAC;IACf,WAAW,EAAE,EAAE;CAChB,CAAC;AAIF;;;;;;;;GAQG;AACH,SAAS,YAAY,CAAC,GAAc;IAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACvB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAE5B,IAAI,SAAS,GAAa,EAAE,CAAC;QAE7B,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QACtD,MAAM,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QAE9D,IAAI,CAAC,aAAa,IAAI,CAAC,SAAS,EAAE,CAAC;YACjC,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACrD,CAAC;aAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/B,SAAS,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC;YAC/B,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;QACzB,CAAC;aAAM,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YAC7B,SAAS,GAAG;gBACV,GAAG,aAAa;gBAChB,GAAI,KAAK,CAAC,IAAI,CACZ,EAAE,MAAM,EAAE,KAAK,GAAG,SAAS,EAAE,EAAE,wEAAwE;gBACvG,GAAG,EAAE,CAAC,CAAC,CACW;aACrB,CAAC;QACJ,CAAC;aAAM,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YAC7B,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YAC1C,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;QACzB,CAAC;QAED,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAChC,CAAC;AACH,CAAC;AAED,MAAM,iBAAiB,GAAwC,CAAC,OAAO,EAAE,EAAE;IACzE,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAC5B,EAAE,EACF,gBAAgB,EAChB,OAAO,CAC+B,CAAC;IAEzC,MAAM,iBAAiB,GACrB,QAAQ,CAAC,OAAO;QAChB,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;YAC9B,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC;YAC3D,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC,OAAO,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAEvD,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;QACpB,MAAM,OAAO,GAAG,IAAI,aAAa,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;YAChD,IAAI,CAAC,OAAO;gBAAE,OAAO;YAErB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;YAC1B,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,CAAC;YAC1D,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/D,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAE5B,iBAAiB;YACjB,IAAI,KAAK,GAAG,QAAQ,CAAC,QAAQ;gBAAE,OAAO,QAAQ,CAAC;YAE/C,mBAAmB;YACnB,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,OAAO,QAAQ,CAAC;YAEzD,oBAAoB;YACpB,IAAI,MAAM,KAAK,MAAM,IAAI,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,OAAO,QAAQ,CAAC;YAEhF,gBAAgB;YAChB,IAAI,iBAAiB,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,OAAO,QAAQ,CAAC;YAExE,sEAAsE;YACtE,+EAA+E;YAC/E,MAAM,IAAI,GAAI,KAAK,CAAC,IAA4B,EAAE,WAAW;gBAC3D,CAAC,CAAC,EAAE,GAAI,KAAK,CAAC,IAA4B,CAAC,WAAW,EAAE;gBACxD,CAAC,CAAC,SAAS,CAAC;YAEd,QAAQ,CAAC,IAAI,CAAC;gBACZ,KAAK;gBACL,IAAI;gBACJ,KAAK;gBACL,SAAS,EAAE,EAAE;gBACb,MAAM;gBACN,GAAG,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,CAAC;aACtB,CAAC,CAAC;YAEH,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,YAAY,CAAC,QAAQ,CAAC,CAAC;QAEvB,kDAAkD;QAClD,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC;QAE9B,6EAA6E;QAE7E,0DAA0D;QAC1D,2DAA2D;QAC3D,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;QAE5C,6EAA6E;QAE7E,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACpB,6DAA6D;YAC7D,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YAE3B,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBAC3B,yDAAyD;gBACzD,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAChC,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "remark-flexible-toc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Remark plugin to expose the table of contents via Vfile.data or via an option reference",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./dist/esm/index.js",
|
|
7
|
+
"main": "./dist/esm/index.js",
|
|
8
|
+
"types": "./dist/esm/index.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"prebuild": "rimraf dist",
|
|
11
|
+
"build": "npm run prebuild && tsc",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"prettier": "prettier --write .",
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"test:ci": "vitest --watch=false",
|
|
16
|
+
"test:file": "vitest without_options.spec.ts",
|
|
17
|
+
"prepack": "npm run build",
|
|
18
|
+
"prepublishOnly": "npm run test:ci && npm run prettier && npm run lint"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/",
|
|
22
|
+
"src/",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/ipikuka/remark-flexible-toc.git"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"unified",
|
|
32
|
+
"mdast",
|
|
33
|
+
"remark",
|
|
34
|
+
"markdown",
|
|
35
|
+
"MDX",
|
|
36
|
+
"plugin",
|
|
37
|
+
"remark-plugin",
|
|
38
|
+
"TOC",
|
|
39
|
+
"table of contents",
|
|
40
|
+
"remark-toc",
|
|
41
|
+
"remark-flexible-toc"
|
|
42
|
+
],
|
|
43
|
+
"author": "ipikuka <talatkuyuk@gmail.com>",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"homepage": "https://github.com/ipikuka/remark-flexible-toc#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/ipikuka/remark-flexible-toc/issues"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/dedent": "^0.7.2",
|
|
51
|
+
"@types/node": "^20.10.5",
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
|
53
|
+
"@typescript-eslint/parser": "^6.16.0",
|
|
54
|
+
"dedent": "^0.7.0",
|
|
55
|
+
"eslint": "^8.56.0",
|
|
56
|
+
"eslint-config-prettier": "^9.1.0",
|
|
57
|
+
"eslint-plugin-prettier": "^5.1.2",
|
|
58
|
+
"prettier": "^3.1.1",
|
|
59
|
+
"rehype-format": "^5.0.0",
|
|
60
|
+
"rehype-stringify": "^10.0.0",
|
|
61
|
+
"remark-gfm": "^4.0.0",
|
|
62
|
+
"remark-parse": "^11.0.0",
|
|
63
|
+
"remark-rehype": "^11.0.0",
|
|
64
|
+
"rimraf": "^5.0.5",
|
|
65
|
+
"typescript": "^5.3.3",
|
|
66
|
+
"unified": "^11.0.4",
|
|
67
|
+
"vitest": "^1.3.0"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@types/mdast": "^4.0.0",
|
|
71
|
+
"github-slugger": "^2.0.0",
|
|
72
|
+
"mdast-util-to-string": "^4.0.0",
|
|
73
|
+
"unist-util-visit": "^5.0.0"
|
|
74
|
+
},
|
|
75
|
+
"sideEffects": false
|
|
76
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { type Plugin } from "unified";
|
|
2
|
+
import { type Root, type HeadingData } from "mdast";
|
|
3
|
+
import { visit, CONTINUE } from "unist-util-visit";
|
|
4
|
+
import GithubSlugger from "github-slugger";
|
|
5
|
+
import { toString } from "mdast-util-to-string";
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
8
|
+
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
11
|
+
export type PartiallyRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
12
|
+
|
|
13
|
+
export type HeadingParent =
|
|
14
|
+
| "root"
|
|
15
|
+
| "blockquote"
|
|
16
|
+
| "footnoteDefinition"
|
|
17
|
+
| "listItem"
|
|
18
|
+
| "container"
|
|
19
|
+
| "mdxJsxFlowElement";
|
|
20
|
+
|
|
21
|
+
export type HeadingDepth = 1 | 2 | 3 | 4 | 5 | 6;
|
|
22
|
+
|
|
23
|
+
export type TocItem = {
|
|
24
|
+
value: string;
|
|
25
|
+
href: string;
|
|
26
|
+
depth: HeadingDepth;
|
|
27
|
+
numbering: number[];
|
|
28
|
+
parent: HeadingParent;
|
|
29
|
+
data?: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* tocName (default: "toc") - the key name which is attached into vfile.data
|
|
34
|
+
* tocRef (default: []) — another way of exposing the tocItems
|
|
35
|
+
* maxDepth (default: 6) — max heading depth to include in the table of contents; this is inclusive: when set to 3, level three headings are included
|
|
36
|
+
* skipLevels (default: [1]) — disallowed heading levels, by default the article h1 is not expected to be in the TOC
|
|
37
|
+
* skipParents (default: []) — disallow headings to be children of certain node types, (the "root" can not be skipped)
|
|
38
|
+
* exclude — headings to skip, wrapped in new RegExp('^(' + value + ')$', 'i'); any heading matching this expression will not be present in the table of contents
|
|
39
|
+
* prefix - the text that will be attached to headings as prefix, like "text-prefix-"
|
|
40
|
+
* callback - It is a callback function to take the array of toc items as an argument
|
|
41
|
+
*/
|
|
42
|
+
export type FlexibleTocOptions = {
|
|
43
|
+
tocName?: string;
|
|
44
|
+
tocRef?: TocItem[];
|
|
45
|
+
maxDepth?: HeadingDepth;
|
|
46
|
+
skipLevels?: HeadingDepth[];
|
|
47
|
+
skipParents?: Exclude<HeadingParent, "root">[];
|
|
48
|
+
exclude?: string | string[];
|
|
49
|
+
prefix?: string;
|
|
50
|
+
callback?: (toc: TocItem[]) => undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type PartiallyRequiredFlexibleTocOptions = Prettify<
|
|
54
|
+
PartiallyRequired<
|
|
55
|
+
FlexibleTocOptions,
|
|
56
|
+
"tocName" | "tocRef" | "maxDepth" | "skipLevels" | "skipParents"
|
|
57
|
+
>
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
const DEFAULT_SETTINGS = {
|
|
61
|
+
tocName: "toc",
|
|
62
|
+
tocRef: [],
|
|
63
|
+
maxDepth: 6,
|
|
64
|
+
skipLevels: [1],
|
|
65
|
+
skipParents: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ExtendedHeadingData = HeadingData & { hProperties: Record<string, unknown> };
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* adds numberings to the TOC items.
|
|
72
|
+
* why "number[]"? It is because up to you joining with dot or dash or slicing the first number (reserved for h1)
|
|
73
|
+
*
|
|
74
|
+
* [1]
|
|
75
|
+
* [1,1]
|
|
76
|
+
* [1,2]
|
|
77
|
+
* [1,2,1]
|
|
78
|
+
*/
|
|
79
|
+
function addNumbering(arr: TocItem[]) {
|
|
80
|
+
for (let i = 0; i < arr.length; i++) {
|
|
81
|
+
const tocItem = arr[i];
|
|
82
|
+
const depth = tocItem.depth;
|
|
83
|
+
|
|
84
|
+
let numbering: number[] = [];
|
|
85
|
+
|
|
86
|
+
const prevObj = i > 0 ? arr[i - 1] : undefined;
|
|
87
|
+
const prevDepth = prevObj ? prevObj.depth : undefined;
|
|
88
|
+
const prevNumbering = prevObj ? prevObj.numbering : undefined;
|
|
89
|
+
|
|
90
|
+
if (!prevNumbering || !prevDepth) {
|
|
91
|
+
numbering = Array.from({ length: depth }, () => 1);
|
|
92
|
+
} else if (depth === prevDepth) {
|
|
93
|
+
numbering = [...prevNumbering];
|
|
94
|
+
numbering[depth - 1]++;
|
|
95
|
+
} else if (depth > prevDepth) {
|
|
96
|
+
numbering = [
|
|
97
|
+
...prevNumbering,
|
|
98
|
+
...(Array.from(
|
|
99
|
+
{ length: depth - prevDepth }, // if depth is more bigger than prevDepth, put more "1" inside the array
|
|
100
|
+
() => 1,
|
|
101
|
+
) as HeadingDepth[]),
|
|
102
|
+
];
|
|
103
|
+
} else if (depth < prevDepth) {
|
|
104
|
+
numbering = prevNumbering.slice(0, depth);
|
|
105
|
+
numbering[depth - 1]++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
tocItem.numbering = numbering;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const RemarkFlexibleToc: Plugin<[FlexibleTocOptions?], Root> = (options) => {
|
|
113
|
+
const settings = Object.assign(
|
|
114
|
+
{},
|
|
115
|
+
DEFAULT_SETTINGS,
|
|
116
|
+
options,
|
|
117
|
+
) as PartiallyRequiredFlexibleTocOptions;
|
|
118
|
+
|
|
119
|
+
const exludeRegexFilter =
|
|
120
|
+
settings.exclude &&
|
|
121
|
+
(Array.isArray(settings.exclude)
|
|
122
|
+
? new RegExp("^(" + settings.exclude.join("|") + ")$", "i")
|
|
123
|
+
: new RegExp("^(" + settings.exclude + ")$", "i"));
|
|
124
|
+
|
|
125
|
+
return (tree, file) => {
|
|
126
|
+
const slugger = new GithubSlugger();
|
|
127
|
+
const tocItems: TocItem[] = [];
|
|
128
|
+
|
|
129
|
+
visit(tree, "heading", (_node, _index, _parent) => {
|
|
130
|
+
if (!_parent) return;
|
|
131
|
+
|
|
132
|
+
const depth = _node.depth;
|
|
133
|
+
const value = toString(_node, { includeImageAlt: false });
|
|
134
|
+
const href = `#${settings.prefix ?? ""}${slugger.slug(value)}`;
|
|
135
|
+
const parent = _parent.type;
|
|
136
|
+
|
|
137
|
+
// maxDepth check
|
|
138
|
+
if (depth > settings.maxDepth) return CONTINUE;
|
|
139
|
+
|
|
140
|
+
// skipLevels check
|
|
141
|
+
if (settings.skipLevels.includes(depth)) return CONTINUE;
|
|
142
|
+
|
|
143
|
+
// skipParents check
|
|
144
|
+
if (parent !== "root" && settings.skipParents.includes(parent)) return CONTINUE;
|
|
145
|
+
|
|
146
|
+
// exclude check
|
|
147
|
+
if (exludeRegexFilter && exludeRegexFilter.test(value)) return CONTINUE;
|
|
148
|
+
|
|
149
|
+
// Other remark plugins can store custom data in node.data.hProperties
|
|
150
|
+
// I omitted node.data.hName and node.data.hChildren since not related with toc
|
|
151
|
+
const data = (_node.data as ExtendedHeadingData)?.hProperties
|
|
152
|
+
? { ...(_node.data as ExtendedHeadingData).hProperties }
|
|
153
|
+
: undefined;
|
|
154
|
+
|
|
155
|
+
tocItems.push({
|
|
156
|
+
value,
|
|
157
|
+
href,
|
|
158
|
+
depth,
|
|
159
|
+
numbering: [],
|
|
160
|
+
parent,
|
|
161
|
+
...(data && { data }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return CONTINUE;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
addNumbering(tocItems);
|
|
168
|
+
|
|
169
|
+
// it is allowed to modify the TOC in the callback
|
|
170
|
+
settings.callback?.(tocItems);
|
|
171
|
+
|
|
172
|
+
// method - 1 for exposing the data via vfile.data **************************
|
|
173
|
+
|
|
174
|
+
// other plugins are not allowed to mutate the exposed TOC
|
|
175
|
+
// The spreading is slower than push but need to fresh copy
|
|
176
|
+
file.data[settings.tocName] = [...tocItems];
|
|
177
|
+
|
|
178
|
+
// method - 2 for exposing the data via reference array *********************
|
|
179
|
+
|
|
180
|
+
if (options?.tocRef) {
|
|
181
|
+
// prevent dublication if the plugin is called more than once
|
|
182
|
+
settings.tocRef.length = 0;
|
|
183
|
+
|
|
184
|
+
tocItems.forEach((tocItem) => {
|
|
185
|
+
// the tocRef is not allowed to mutate the vfile.data.toc
|
|
186
|
+
settings.tocRef.push(tocItem);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export default RemarkFlexibleToc;
|