@0dep/toc 1.0.1 → 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 +32 -0
- package/README.md +114 -20
- package/bin/toc.js +100 -43
- package/index.js +192 -82
- package/package.json +11 -16
- package/types/index.d.ts +79 -59
- package/types/index.d.ts.map +1 -23
- package/index.cjs +0 -332
package/index.js
CHANGED
|
@@ -1,64 +1,67 @@
|
|
|
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);
|
|
7
|
+
/** @type {Map<string, ReturnType<typeof analyse>>} */
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
const LINK = /\[([^\]]*)\]\(#([^)\s]+)/g;
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
|
-
*
|
|
9
|
-
* pair lists every heading below its own start marker. A pair is left alone when it is unbalanced, has a
|
|
10
|
-
* problem on its start marker, or has no headings below it, and nothing outside the pairs is ever touched.
|
|
11
|
-
* The start marker line is kept as written, options included.
|
|
12
|
+
* The markdown with the toc between every `<!-- toc -->` and `<!-- /toc -->` pair regenerated.
|
|
12
13
|
* @param {string} source
|
|
13
14
|
* @returns {string}
|
|
14
15
|
*/
|
|
15
16
|
export function buildToc(source) {
|
|
16
|
-
const { lines, eol } =
|
|
17
|
-
const { headlines, markers } = scan(lines);
|
|
17
|
+
const { bom, lines, eol, headlines, markers } = analyse(source);
|
|
18
18
|
/** @type {string[]} */
|
|
19
19
|
const out = [];
|
|
20
20
|
let cursor = 0;
|
|
21
21
|
for (const { start, end, options, problem } of markers) {
|
|
22
22
|
if (start === -1 || end === -1 || problem) continue;
|
|
23
|
-
const listed = headlines
|
|
23
|
+
const listed = headingsBelow(headlines, start, options);
|
|
24
24
|
if (listed.length === 0) continue;
|
|
25
25
|
out.push(...lines.slice(cursor, start), renderBlock(listed, lines[start], options, eol));
|
|
26
26
|
cursor = end + 1;
|
|
27
27
|
}
|
|
28
28
|
out.push(...lines.slice(cursor));
|
|
29
|
-
return out.join(eol);
|
|
29
|
+
return bom + out.join(eol);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
* block can be pasted into a document without markers. Returns an empty string when there is nothing to list.
|
|
33
|
+
* The toc block, markers included, for the headings below `fromLine`, empty when there is nothing to list.
|
|
35
34
|
* @param {string} source
|
|
36
|
-
* @param {number} [fromLine] zero based
|
|
37
|
-
* @param {TocOptions} [options]
|
|
35
|
+
* @param {number} [fromLine] zero based
|
|
36
|
+
* @param {TocOptions} [options]
|
|
38
37
|
* @returns {string}
|
|
39
38
|
*/
|
|
40
39
|
export function renderToc(source, fromLine = -1, options = {}) {
|
|
41
|
-
const {
|
|
42
|
-
const listed =
|
|
40
|
+
const { eol, headlines } = analyse(source);
|
|
41
|
+
const listed = headingsBelow(headlines, fromLine, options);
|
|
43
42
|
return listed.length === 0 ? '' : renderBlock(listed, formatMarker(options), options, eol);
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
|
-
* Every marker pair in document order
|
|
48
|
-
* pairs with the first end marker after it. A missing side is -1: a start marker without an end marker, or an
|
|
49
|
-
* end marker with no open start marker before it. `options` holds the recognised options written on the start
|
|
50
|
-
* marker and `problem`, only present when there is one, says why the marker cannot be used.
|
|
46
|
+
* Every marker pair in document order, shared with later calls for the same source.
|
|
51
47
|
* @param {string} source
|
|
52
48
|
* @returns {Marker[]}
|
|
53
49
|
*/
|
|
54
50
|
export function findMarkers(source) {
|
|
55
|
-
return
|
|
51
|
+
return analyse(source).markers;
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
55
|
+
* Every link to an anchor in the document, in order, shared with later calls for the same source.
|
|
56
|
+
* @param {string} source
|
|
57
|
+
* @returns {Anchor[]}
|
|
58
|
+
*/
|
|
59
|
+
export function findAnchors(source) {
|
|
60
|
+
return analyse(source).anchors;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The GitHub anchor slug of a heading's rendered text.
|
|
62
65
|
* @param {string} text
|
|
63
66
|
* @returns {string}
|
|
64
67
|
*/
|
|
@@ -70,7 +73,7 @@ export function slugify(text) {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/**
|
|
73
|
-
*
|
|
76
|
+
* The text GitHub renders for a heading's inline markdown.
|
|
74
77
|
* @param {string} markdown
|
|
75
78
|
* @returns {string}
|
|
76
79
|
*/
|
|
@@ -85,33 +88,51 @@ export function headingText(markdown) {
|
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/**
|
|
88
|
-
*
|
|
89
|
-
* written the way the rest of the file is and a Windows authored file does not end up with mixed endings.
|
|
91
|
+
* The lines and the scan of the source.
|
|
90
92
|
* @param {string} source
|
|
91
|
-
* @returns {{ lines: string[], eol: string }}
|
|
93
|
+
* @returns {{ bom: string, lines: string[], eol: string, headlines: Headline[], markers: Marker[], anchors: Anchor[] }}
|
|
94
|
+
*/
|
|
95
|
+
function analyse(source) {
|
|
96
|
+
let result = cache.get(source);
|
|
97
|
+
if (!result) {
|
|
98
|
+
const { bom, lines, eol } = splitLines(source);
|
|
99
|
+
result = { bom, lines, eol, ...scan(lines) };
|
|
100
|
+
cache.clear();
|
|
101
|
+
cache.set(source, result);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The lines of the source, its byte order mark if any, and its line ending.
|
|
108
|
+
* @param {string} source
|
|
109
|
+
* @returns {{ bom: string, lines: string[], eol: string }}
|
|
92
110
|
*/
|
|
93
111
|
function splitLines(source) {
|
|
94
|
-
|
|
112
|
+
const bom = source.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
113
|
+
return { bom, lines: source.slice(bom.length).split(/\r?\n/), eol: source.includes('\r\n') ? '\r\n' : '\n' };
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
/**
|
|
98
|
-
*
|
|
99
|
-
* marker is a line holding nothing but the comment, indented at most three spaces like a heading, since four
|
|
100
|
-
* make an indented code block. A start marker, `<!-- toc -->` with optional options before the closing `-->`,
|
|
101
|
-
* opens a pair that the first end marker after it closes; a start marker inside an open pair is content and an end marker outside a pair is
|
|
102
|
-
* reported with start -1.
|
|
117
|
+
* The headings, marker pairs and anchor links of the lines.
|
|
103
118
|
* @param {string[]} lines
|
|
104
|
-
* @returns {{ headlines:
|
|
119
|
+
* @returns {{ headlines: Headline[], markers: Marker[], anchors: Anchor[] }}
|
|
105
120
|
*/
|
|
106
121
|
function scan(lines) {
|
|
107
|
-
/** @type {
|
|
122
|
+
/** @type {Headline[]} */
|
|
108
123
|
const headlines = [];
|
|
109
124
|
/** @type {Marker[]} */
|
|
110
125
|
const markers = [];
|
|
126
|
+
/** @type {Array<{ line: number, text: string, anchor: string }>} */
|
|
127
|
+
const links = [];
|
|
128
|
+
/** @type {Set<string>} */
|
|
129
|
+
const ids = new Set();
|
|
111
130
|
/** @type {{ char: string, length: number } | null} */
|
|
112
131
|
let fence = null;
|
|
113
132
|
/** @type {Marker | null} */
|
|
114
133
|
let open = null;
|
|
134
|
+
let paragraph = false;
|
|
135
|
+
let comment = false;
|
|
115
136
|
|
|
116
137
|
for (const [i, line] of lines.entries()) {
|
|
117
138
|
const fenceMatch = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
@@ -123,8 +144,32 @@ function scan(lines) {
|
|
|
123
144
|
}
|
|
124
145
|
if (fenceMatch && !(fenceMatch[1][0] === '`' && fenceMatch[2].includes('`'))) {
|
|
125
146
|
fence = { char: fenceMatch[1][0], length: fenceMatch[1].length };
|
|
147
|
+
paragraph = false;
|
|
126
148
|
continue;
|
|
127
149
|
}
|
|
150
|
+
if (!paragraph && /^(?: {4}|\t)/.test(line)) continue;
|
|
151
|
+
paragraph = line.trim() !== '';
|
|
152
|
+
|
|
153
|
+
const { text: inline, restore } = protect(line);
|
|
154
|
+
let visible = inline;
|
|
155
|
+
if (comment) {
|
|
156
|
+
const close = visible.indexOf('-->');
|
|
157
|
+
if (close === -1) continue;
|
|
158
|
+
visible = visible.slice(close + 3);
|
|
159
|
+
comment = false;
|
|
160
|
+
}
|
|
161
|
+
visible = visible.replace(/<!--[\s\S]*?-->/g, '');
|
|
162
|
+
const start = visible.indexOf('<!--');
|
|
163
|
+
if (start !== -1) {
|
|
164
|
+
visible = visible.slice(0, start);
|
|
165
|
+
comment = true;
|
|
166
|
+
}
|
|
167
|
+
for (const [, tag, attributes] of visible.matchAll(/<([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*)>/g)) {
|
|
168
|
+
for (const [, name, quoted, single] of attributes.matchAll(/(?:^|\s)(id|name)=(?:"([^"]*)"|'([^']*)')/g)) {
|
|
169
|
+
if (name === 'id' || tag.toLowerCase() === 'a') ids.add(quoted ?? single);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const [, text, anchor] of visible.matchAll(LINK)) links.push({ line: i, text: restore(text), anchor: restore(anchor) });
|
|
128
173
|
|
|
129
174
|
const startMatch = /^ {0,3}<!--\s*toc(?:\s+(.*?))?\s*-->\s*$/.exec(line);
|
|
130
175
|
if (startMatch) {
|
|
@@ -140,42 +185,102 @@ function scan(lines) {
|
|
|
140
185
|
|
|
141
186
|
const atx = /^ {0,3}(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
142
187
|
if (atx) {
|
|
143
|
-
headlines.push({ line: i, level: atx[1].length, markdown: atx[2] });
|
|
188
|
+
headlines.push({ line: i, level: atx[1].length, markdown: atx[2], slug: '' });
|
|
144
189
|
continue;
|
|
145
190
|
}
|
|
146
191
|
|
|
147
192
|
const setext = /^ {0,3}(=+|-+)\s*$/.exec(line);
|
|
148
193
|
if (setext && i > 0 && isParagraphText(lines[i - 1])) {
|
|
149
|
-
headlines.push({ line: i - 1, level: setext[1][0] === '=' ? 1 : 2, markdown: lines[i - 1].trim() });
|
|
194
|
+
headlines.push({ line: i - 1, level: setext[1][0] === '=' ? 1 : 2, markdown: lines[i - 1].trim(), slug: '' });
|
|
150
195
|
}
|
|
151
196
|
}
|
|
152
|
-
|
|
197
|
+
assignSlugs(headlines);
|
|
198
|
+
return { headlines, markers, anchors: resolveAnchors(links, headlines, ids) };
|
|
153
199
|
}
|
|
154
200
|
|
|
155
201
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* lines around it so it renders as markdown, and any other attributes from the marker on the summary element.
|
|
161
|
-
* @param {Array<{ level: number, markdown: string }>} headlines
|
|
162
|
-
* @param {string} startLine the start marker as written in the document
|
|
163
|
-
* @param {TocOptions} options
|
|
164
|
-
* @param {string} [eol] line ending, LF by default
|
|
202
|
+
* @param {Array<{ line: number, text: string, anchor: string }>} links
|
|
203
|
+
* @param {Headline[]} headlines
|
|
204
|
+
* @param {Set<string>} ids
|
|
205
|
+
* @returns {Anchor[]}
|
|
165
206
|
*/
|
|
166
|
-
function
|
|
207
|
+
function resolveAnchors(links, headlines, ids) {
|
|
208
|
+
const targets = new Set([...headlines.map((h) => h.slug), ...ids]);
|
|
209
|
+
return links.map(({ line, text, anchor }) => {
|
|
210
|
+
const decoded = decodeAnchor(anchor);
|
|
211
|
+
if (targets.has(anchor) || targets.has(decoded)) return { line, text, anchor, valid: true };
|
|
212
|
+
const candidates = new Set([slugify(headingText(decoded)), slugify(headingText(text))].filter((c) => targets.has(c)));
|
|
213
|
+
if (candidates.size !== 1) return { line, text, anchor, valid: false };
|
|
214
|
+
return { line, text, anchor, valid: false, suggestion: [...candidates][0] };
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @param {string} anchor */
|
|
219
|
+
function decodeAnchor(anchor) {
|
|
220
|
+
try {
|
|
221
|
+
return decodeURIComponent(anchor);
|
|
222
|
+
} catch {
|
|
223
|
+
return anchor;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Set the slug of every heading.
|
|
229
|
+
* @param {Headline[]} headlines
|
|
230
|
+
*/
|
|
231
|
+
function assignSlugs(headlines) {
|
|
167
232
|
/** @type {Record<string, number>} */
|
|
168
233
|
const occurrences = {};
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
minLevel = Math.min(minLevel, level);
|
|
172
|
-
const base = slugify(headingText(markdown));
|
|
234
|
+
for (const headline of headlines) {
|
|
235
|
+
const base = slugify(headingText(headline.markdown));
|
|
173
236
|
let slug = base;
|
|
174
237
|
while (Object.hasOwn(occurrences, slug)) {
|
|
175
238
|
occurrences[base]++;
|
|
176
239
|
slug = `${base}-${occurrences[base]}`;
|
|
177
240
|
}
|
|
178
241
|
occurrences[slug] = 0;
|
|
242
|
+
headline.slug = slug;
|
|
243
|
+
}
|
|
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
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The toc block for the headings, between the start marker line and the end marker.
|
|
275
|
+
* @param {Headline[]} headlines
|
|
276
|
+
* @param {string} startLine
|
|
277
|
+
* @param {TocOptions} options
|
|
278
|
+
* @param {string} [eol]
|
|
279
|
+
*/
|
|
280
|
+
function renderBlock(headlines, startLine, options, eol = '\n') {
|
|
281
|
+
let minLevel = Infinity;
|
|
282
|
+
const tocLines = headlines.map(({ level, markdown, slug }) => {
|
|
283
|
+
minLevel = Math.min(minLevel, level);
|
|
179
284
|
return `${' '.repeat(level - minLevel)}- [${headingLabel(markdown)}](#${slug})`;
|
|
180
285
|
});
|
|
181
286
|
const details = options.collapsible ?? options.collapsed;
|
|
@@ -189,12 +294,9 @@ function renderBlock(headlines, startLine, options, eol = '\n') {
|
|
|
189
294
|
}
|
|
190
295
|
|
|
191
296
|
/**
|
|
192
|
-
*
|
|
193
|
-
* (`collapsed="Contents"`), and any other well-formed `name="value"` as an attribute for the summary element.
|
|
194
|
-
* Anything else, bare names that are not options, unquoted values, malformed names, is a problem, as is a
|
|
195
|
-
* combination that makes no sense. A marker with a problem is never used.
|
|
297
|
+
* The options written on a start marker, with a problem when they cannot be used and a warning when one is ignored.
|
|
196
298
|
* @param {string | undefined} text
|
|
197
|
-
* @returns {{ options: TocOptions, problem?: string }}
|
|
299
|
+
* @returns {{ options: TocOptions, problem?: string, warning?: string }}
|
|
198
300
|
*/
|
|
199
301
|
function parseOptions(text) {
|
|
200
302
|
/** @type {TocOptions} */
|
|
@@ -204,24 +306,29 @@ function parseOptions(text) {
|
|
|
204
306
|
/** @type {string[]} */
|
|
205
307
|
const unknown = [];
|
|
206
308
|
for (const [token, name, value] of (text ?? '').matchAll(/([^\s="]+)(?:="([^"]*)")?(?=\s|$)|\S+/g)) {
|
|
207
|
-
if (name
|
|
309
|
+
if (name === 'levels') options.levels = value ?? '';
|
|
310
|
+
else if (name && KNOWN_OPTIONS.includes(name)) options[/** @type {'collapsible' | 'collapsed'} */ (name)] = value ?? true;
|
|
208
311
|
else if (name && value !== undefined && /^[a-zA-Z][\w:.-]*$/.test(name)) attributes[name] = value;
|
|
209
312
|
else unknown.push(token);
|
|
210
313
|
}
|
|
211
314
|
const names = Object.keys(attributes);
|
|
212
315
|
if (names.length) options.attributes = attributes;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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`;
|
|
216
320
|
}
|
|
217
|
-
if (
|
|
218
|
-
|
|
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`;
|
|
219
326
|
}
|
|
220
|
-
return
|
|
327
|
+
return parsed;
|
|
221
328
|
}
|
|
222
329
|
|
|
223
330
|
/**
|
|
224
|
-
* The start marker line for the
|
|
331
|
+
* The start marker line for the options.
|
|
225
332
|
* @param {TocOptions} options
|
|
226
333
|
*/
|
|
227
334
|
function formatMarker(options) {
|
|
@@ -235,15 +342,14 @@ function formatMarker(options) {
|
|
|
235
342
|
|
|
236
343
|
/**
|
|
237
344
|
* @param {Record<string, string>} attributes
|
|
238
|
-
* @returns {string[]}
|
|
345
|
+
* @returns {string[]}
|
|
239
346
|
*/
|
|
240
347
|
function formatAttributes(attributes) {
|
|
241
348
|
return Object.entries(attributes).map(([name, value]) => `${name}="${value}"`);
|
|
242
349
|
}
|
|
243
350
|
|
|
244
351
|
/**
|
|
245
|
-
* The
|
|
246
|
-
* cannot nest inside the toc link.
|
|
352
|
+
* The link text used in the toc for a heading.
|
|
247
353
|
* @param {string} markdown
|
|
248
354
|
* @returns {string}
|
|
249
355
|
*/
|
|
@@ -253,8 +359,7 @@ function headingLabel(markdown) {
|
|
|
253
359
|
}
|
|
254
360
|
|
|
255
361
|
/**
|
|
256
|
-
*
|
|
257
|
-
* (with the escaped character) by placeholders so the inline markdown passes leave them alone.
|
|
362
|
+
* The markdown with code spans and backslash escapes swapped for placeholders, and a function to put them back.
|
|
258
363
|
* @param {string} markdown
|
|
259
364
|
* @param {{ keepCodeSpans?: boolean }} [options]
|
|
260
365
|
*/
|
|
@@ -274,8 +379,7 @@ function protect(markdown, { keepCodeSpans = false } = {}) {
|
|
|
274
379
|
}
|
|
275
380
|
|
|
276
381
|
/**
|
|
277
|
-
*
|
|
278
|
-
* not only spaces, as CommonMark does.
|
|
382
|
+
* The code span content unpadded as CommonMark renders it.
|
|
279
383
|
* @param {string} code
|
|
280
384
|
*/
|
|
281
385
|
function unpadCodeSpan(code) {
|
|
@@ -289,8 +393,7 @@ function stripClosingHashes(markdown) {
|
|
|
289
393
|
}
|
|
290
394
|
|
|
291
395
|
/**
|
|
292
|
-
*
|
|
293
|
-
* the toc label since a link cannot nest inside the toc link.
|
|
396
|
+
* The text with links, images and autolinks flattened to their text or url.
|
|
294
397
|
* @param {string} text
|
|
295
398
|
*/
|
|
296
399
|
function stripLinks(text) {
|
|
@@ -308,15 +411,22 @@ function isParagraphText(line) {
|
|
|
308
411
|
}
|
|
309
412
|
|
|
310
413
|
/**
|
|
311
|
-
* Options written on a start marker
|
|
312
|
-
*
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
414
|
+
* Options written on a start marker, each `true` or a summary text, and the other attributes for the summary element.
|
|
415
|
+
* @typedef {{ collapsible?: true | string, collapsed?: true | string, levels?: string, attributes?: Record<string, string> }} TocOptions
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* A heading with its zero based line, level, inline markdown and GitHub slug.
|
|
420
|
+
* @typedef {{ line: number, level: number, markdown: string, slug: string }} Headline
|
|
421
|
+
*/
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* A link to an anchor with its zero based line, text, anchor as written, whether it has a target and, when it
|
|
425
|
+
* has none and one heading clearly matches, a suggestion.
|
|
426
|
+
* @typedef {{ line: number, text: string, anchor: string, valid: boolean, suggestion?: string }} Anchor
|
|
316
427
|
*/
|
|
317
428
|
|
|
318
429
|
/**
|
|
319
|
-
* A marker pair
|
|
320
|
-
*
|
|
321
|
-
* @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
|
|
322
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 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
|
}
|