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 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":"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"}
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;