@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/types/index.d.ts
CHANGED
|
@@ -1,60 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The markdown with the toc between every `<!-- toc -->` and `<!-- /toc -->` pair regenerated.
|
|
3
|
+
* @param {string} source
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
export function buildToc(source: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* The toc block, markers included, for the headings below `fromLine`, empty when there is nothing to list.
|
|
9
|
+
* @param {string} source
|
|
10
|
+
* @param {number} [fromLine] zero based
|
|
11
|
+
* @param {TocOptions} [options]
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function renderToc(source: string, fromLine?: number, options?: TocOptions): string;
|
|
15
|
+
/**
|
|
16
|
+
* Every marker pair in document order, shared with later calls for the same source.
|
|
17
|
+
* @param {string} source
|
|
18
|
+
* @returns {Marker[]}
|
|
19
|
+
*/
|
|
20
|
+
export function findMarkers(source: string): Marker[];
|
|
21
|
+
/**
|
|
22
|
+
* Every link to an anchor in the document, in order, shared with later calls for the same source.
|
|
23
|
+
* @param {string} source
|
|
24
|
+
* @returns {Anchor[]}
|
|
25
|
+
*/
|
|
26
|
+
export function findAnchors(source: string): Anchor[];
|
|
27
|
+
/**
|
|
28
|
+
* The GitHub anchor slug of a heading's rendered text.
|
|
29
|
+
* @param {string} text
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function slugify(text: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* The text GitHub renders for a heading's inline markdown.
|
|
35
|
+
* @param {string} markdown
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function headingText(markdown: string): string;
|
|
39
|
+
export const TOC_START: "<!-- toc -->";
|
|
40
|
+
export const TOC_END: "<!-- /toc -->";
|
|
41
|
+
/**
|
|
42
|
+
* Options written on a start marker, each `true` or a summary text, and the other attributes for the summary element.
|
|
43
|
+
*/
|
|
44
|
+
export type TocOptions = {
|
|
45
|
+
collapsible?: true | string;
|
|
46
|
+
collapsed?: true | string;
|
|
47
|
+
levels?: string;
|
|
48
|
+
attributes?: Record<string, string>;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A heading with its zero based line, level, inline markdown and GitHub slug.
|
|
52
|
+
*/
|
|
53
|
+
export type Headline = {
|
|
54
|
+
line: number;
|
|
55
|
+
level: number;
|
|
56
|
+
markdown: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A link to an anchor with its zero based line, text, anchor as written, whether it has a target and, when it
|
|
61
|
+
* has none and one heading clearly matches, a suggestion.
|
|
62
|
+
*/
|
|
63
|
+
export type Anchor = {
|
|
64
|
+
line: number;
|
|
65
|
+
text: string;
|
|
66
|
+
anchor: string;
|
|
67
|
+
valid: boolean;
|
|
68
|
+
suggestion?: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* A marker pair as zero based lines, -1 for a missing side, with its options, a problem when they cannot be used and a warning when one is ignored.
|
|
72
|
+
*/
|
|
73
|
+
export type Marker = {
|
|
74
|
+
start: number;
|
|
75
|
+
end: number;
|
|
76
|
+
options: TocOptions;
|
|
77
|
+
problem?: string;
|
|
78
|
+
warning?: string;
|
|
79
|
+
};
|
|
60
80
|
//# sourceMappingURL=index.d.ts.map
|
package/types/index.d.ts.map
CHANGED
|
@@ -1,23 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"file": "index.d.ts",
|
|
4
|
-
"names": [
|
|
5
|
-
"buildToc",
|
|
6
|
-
"renderToc",
|
|
7
|
-
"findMarkers",
|
|
8
|
-
"slugify",
|
|
9
|
-
"headingText",
|
|
10
|
-
"TOC_START",
|
|
11
|
-
"TOC_END",
|
|
12
|
-
"TocOptions",
|
|
13
|
-
"Marker"
|
|
14
|
-
],
|
|
15
|
-
"sources": [
|
|
16
|
-
"../index.js"
|
|
17
|
-
],
|
|
18
|
-
"sourcesContent": [
|
|
19
|
-
null
|
|
20
|
-
],
|
|
21
|
-
"mappings": ";;;;;;;iBAcgBA,QAAQA;;;;;;;iBAyBRC,SAASA;;;;;;;iBAcTC,WAAWA;;;;;;iBAWXC,OAAOA;;;;iBAYPC,WAAWA;cA5EdC,SAASA;cACTC,OAAOA;;;;;;;aAyT0FC,UAAUA;;;;;;;;;aAMpCC,MAAMA",
|
|
22
|
-
"ignoreList": []
|
|
23
|
-
}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.js"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,iCAHW,MAAM,GACJ,MAAM,CAgBlB;AAED;;;;;;GAMG;AACH,kCALW,MAAM,aACN,MAAM,YACN,UAAU,GACR,MAAM,CAMlB;AAED;;;;GAIG;AACH,oCAHW,MAAM,GACJ,MAAM,EAAE,CAIpB;AAED;;;;GAIG;AACH,oCAHW,MAAM,GACJ,MAAM,EAAE,CAIpB;AAED;;;;GAIG;AACH,8BAHW,MAAM,GACJ,MAAM,CAOlB;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAUlB;AAvFD,wBAAyB,cAAc,CAAC;AACxC,sBAAuB,eAAe,CAAC;;;;yBA6Z1B;IAAE,WAAW,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE;;;;uBAKhH;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;;;;;qBAM/D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE;;;;qBAKnF;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,UAAU,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE"}
|
package/index.cjs
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const TOC_START = '<!-- toc -->';
|
|
4
|
-
const TOC_END = '<!-- /toc -->';
|
|
5
|
-
const DEFAULT_SUMMARY = 'Table of contents';
|
|
6
|
-
const KNOWN_OPTIONS = ['collapsible', 'collapsed'];
|
|
7
|
-
const NUL = String.fromCharCode(0);
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Return the markdown with the toc between every `<!-- toc -->` and `<!-- /toc -->` pair regenerated. Each
|
|
11
|
-
* pair lists every heading below its own start marker. A pair is left alone when it is unbalanced, has a
|
|
12
|
-
* problem on its start marker, or has no headings below it, and nothing outside the pairs is ever touched.
|
|
13
|
-
* The start marker line is kept as written, options included.
|
|
14
|
-
* @param {string} source
|
|
15
|
-
* @returns {string}
|
|
16
|
-
*/
|
|
17
|
-
function buildToc(source) {
|
|
18
|
-
const { lines, eol } = splitLines(source);
|
|
19
|
-
const { headlines, markers } = scan(lines);
|
|
20
|
-
/** @type {string[]} */
|
|
21
|
-
const out = [];
|
|
22
|
-
let cursor = 0;
|
|
23
|
-
for (const { start, end, options, problem } of markers) {
|
|
24
|
-
if (start === -1 || end === -1 || problem) continue;
|
|
25
|
-
const listed = headlines.filter((h) => h.line > start);
|
|
26
|
-
if (listed.length === 0) continue;
|
|
27
|
-
out.push(...lines.slice(cursor, start), renderBlock(listed, lines[start], options, eol));
|
|
28
|
-
cursor = end + 1;
|
|
29
|
-
}
|
|
30
|
-
out.push(...lines.slice(cursor));
|
|
31
|
-
return out.join(eol);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Return the toc block, markers included, for the headings below `fromLine`, by default every heading so the
|
|
36
|
-
* block can be pasted into a document without markers. Returns an empty string when there is nothing to list.
|
|
37
|
-
* @param {string} source
|
|
38
|
-
* @param {number} [fromLine] zero based line number, typically the start marker's
|
|
39
|
-
* @param {TocOptions} [options] rendered into the start marker, e.g. `{ collapsed: 'Contents' }`
|
|
40
|
-
* @returns {string}
|
|
41
|
-
*/
|
|
42
|
-
function renderToc(source, fromLine = -1, options = {}) {
|
|
43
|
-
const { lines, eol } = splitLines(source);
|
|
44
|
-
const listed = scan(lines).headlines.filter((h) => h.line > fromLine);
|
|
45
|
-
return listed.length === 0 ? '' : renderBlock(listed, formatMarker(options), options, eol);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Every marker pair in document order as zero based line numbers, outside fenced code blocks. A start marker
|
|
50
|
-
* pairs with the first end marker after it. A missing side is -1: a start marker without an end marker, or an
|
|
51
|
-
* end marker with no open start marker before it. `options` holds the recognised options written on the start
|
|
52
|
-
* marker and `problem`, only present when there is one, says why the marker cannot be used.
|
|
53
|
-
* @param {string} source
|
|
54
|
-
* @returns {Marker[]}
|
|
55
|
-
*/
|
|
56
|
-
function findMarkers(source) {
|
|
57
|
-
return scan(splitLines(source).lines).markers;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Slug a heading's rendered text the way github-slugger does: lowercase, drop everything that is not a
|
|
62
|
-
* letter, number, mark, space, hyphen or underscore, then turn each space into a hyphen. Nothing is trimmed
|
|
63
|
-
* or collapsed.
|
|
64
|
-
* @param {string} text
|
|
65
|
-
* @returns {string}
|
|
66
|
-
*/
|
|
67
|
-
function slugify(text) {
|
|
68
|
-
return text
|
|
69
|
-
.toLowerCase()
|
|
70
|
-
.replace(/[^\p{L}\p{N}\p{M} _-]/gu, '')
|
|
71
|
-
.replace(/ /g, '-');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Reduce a heading's inline markdown to the text GitHub renders and slugs.
|
|
76
|
-
* @param {string} markdown
|
|
77
|
-
* @returns {string}
|
|
78
|
-
*/
|
|
79
|
-
function headingText(markdown) {
|
|
80
|
-
const { text, restore } = protect(stripClosingHashes(markdown));
|
|
81
|
-
const rendered = stripLinks(text)
|
|
82
|
-
.replace(/<\/?[a-zA-Z][^>]*>/g, '')
|
|
83
|
-
.replace(/~~(?=\S)(.+?)(?<=\S)~~/g, '$1')
|
|
84
|
-
.replace(/\*+(?=\S)|(?<=\S)\*+/g, '')
|
|
85
|
-
.replace(/(?<![\p{L}\p{N}_])_+(?=\S)|(?<=\S)_+(?![\p{L}\p{N}_])/gu, '');
|
|
86
|
-
return restore(rendered).trim();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Split the source into lines and remember its line ending, CRLF when the source has any, so the toc is
|
|
91
|
-
* written the way the rest of the file is and a Windows authored file does not end up with mixed endings.
|
|
92
|
-
* @param {string} source
|
|
93
|
-
* @returns {{ lines: string[], eol: string }}
|
|
94
|
-
*/
|
|
95
|
-
function splitLines(source) {
|
|
96
|
-
return { lines: source.split(/\r?\n/), eol: source.includes('\r\n') ? '\r\n' : '\n' };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Scan the lines for every marker pair and every ATX and setext heading, all outside fenced code blocks. A
|
|
101
|
-
* marker is a line holding nothing but the comment, indented at most three spaces like a heading, since four
|
|
102
|
-
* make an indented code block. A start marker, `<!-- toc -->` with optional options before the closing `-->`,
|
|
103
|
-
* 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
|
|
104
|
-
* reported with start -1.
|
|
105
|
-
* @param {string[]} lines
|
|
106
|
-
* @returns {{ headlines: Array<{ line: number, level: number, markdown: string }>, markers: Marker[] }}
|
|
107
|
-
*/
|
|
108
|
-
function scan(lines) {
|
|
109
|
-
/** @type {Array<{ line: number, level: number, markdown: string }>} */
|
|
110
|
-
const headlines = [];
|
|
111
|
-
/** @type {Marker[]} */
|
|
112
|
-
const markers = [];
|
|
113
|
-
/** @type {{ char: string, length: number } | null} */
|
|
114
|
-
let fence = null;
|
|
115
|
-
/** @type {Marker | null} */
|
|
116
|
-
let open = null;
|
|
117
|
-
|
|
118
|
-
for (const [i, line] of lines.entries()) {
|
|
119
|
-
const fenceMatch = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
120
|
-
if (fence) {
|
|
121
|
-
if (fenceMatch && fenceMatch[1][0] === fence.char && fenceMatch[1].length >= fence.length && !fenceMatch[2].trim()) {
|
|
122
|
-
fence = null;
|
|
123
|
-
}
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (fenceMatch && !(fenceMatch[1][0] === '`' && fenceMatch[2].includes('`'))) {
|
|
127
|
-
fence = { char: fenceMatch[1][0], length: fenceMatch[1].length };
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const startMatch = /^ {0,3}<!--\s*toc(?:\s+(.*?))?\s*-->\s*$/.exec(line);
|
|
132
|
-
if (startMatch) {
|
|
133
|
-
if (!open) markers.push((open = { start: i, end: -1, ...parseOptions(startMatch[1]) }));
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (/^ {0,3}<!--\s*\/toc\s*-->\s*$/.test(line)) {
|
|
137
|
-
if (open) open.end = i;
|
|
138
|
-
else markers.push({ start: -1, end: i, options: {} });
|
|
139
|
-
open = null;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const atx = /^ {0,3}(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
144
|
-
if (atx) {
|
|
145
|
-
headlines.push({ line: i, level: atx[1].length, markdown: atx[2] });
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const setext = /^ {0,3}(=+|-+)\s*$/.exec(line);
|
|
150
|
-
if (setext && i > 0 && isParagraphText(lines[i - 1])) {
|
|
151
|
-
headlines.push({ line: i - 1, level: setext[1][0] === '=' ? 1 : 2, markdown: lines[i - 1].trim() });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { headlines, markers };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* The toc block for the given headings, wrapped in the given start marker line and the end marker. Indentation
|
|
159
|
-
* is relative to the shallowest level listed so far, so the list never starts indented, which markdown would
|
|
160
|
-
* render flat anyway, and duplicate slugs get github style `-1`, `-2` suffixes. With
|
|
161
|
-
* `collapsible` or `collapsed` the list goes inside a details element, open or closed to start with, blank
|
|
162
|
-
* lines around it so it renders as markdown, and any other attributes from the marker on the summary element.
|
|
163
|
-
* @param {Array<{ level: number, markdown: string }>} headlines
|
|
164
|
-
* @param {string} startLine the start marker as written in the document
|
|
165
|
-
* @param {TocOptions} options
|
|
166
|
-
* @param {string} [eol] line ending, LF by default
|
|
167
|
-
*/
|
|
168
|
-
function renderBlock(headlines, startLine, options, eol = '\n') {
|
|
169
|
-
/** @type {Record<string, number>} */
|
|
170
|
-
const occurrences = {};
|
|
171
|
-
let minLevel = Infinity;
|
|
172
|
-
const tocLines = headlines.map(({ level, markdown }) => {
|
|
173
|
-
minLevel = Math.min(minLevel, level);
|
|
174
|
-
const base = slugify(headingText(markdown));
|
|
175
|
-
let slug = base;
|
|
176
|
-
while (Object.hasOwn(occurrences, slug)) {
|
|
177
|
-
occurrences[base]++;
|
|
178
|
-
slug = `${base}-${occurrences[base]}`;
|
|
179
|
-
}
|
|
180
|
-
occurrences[slug] = 0;
|
|
181
|
-
return `${' '.repeat(level - minLevel)}- [${headingLabel(markdown)}](#${slug})`;
|
|
182
|
-
});
|
|
183
|
-
const details = options.collapsible ?? options.collapsed;
|
|
184
|
-
if (details === undefined) return [startLine, '', ...tocLines, '', TOC_END].join(eol);
|
|
185
|
-
const summary = details === true ? DEFAULT_SUMMARY : details;
|
|
186
|
-
const tag = options.collapsible === undefined ? '<details>' : '<details open>';
|
|
187
|
-
const attrs = formatAttributes(options.attributes ?? {})
|
|
188
|
-
.map((a) => ` ${a}`)
|
|
189
|
-
.join('');
|
|
190
|
-
return [startLine, tag, `<summary${attrs}>${summary}</summary>`, '', ...tocLines, '', '</details>', TOC_END].join(eol);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Parse what is written on a start marker: the options as bare names (`collapsed`) or quoted values
|
|
195
|
-
* (`collapsed="Contents"`), and any other well-formed `name="value"` as an attribute for the summary element.
|
|
196
|
-
* Anything else, bare names that are not options, unquoted values, malformed names, is a problem, as is a
|
|
197
|
-
* combination that makes no sense. A marker with a problem is never used.
|
|
198
|
-
* @param {string | undefined} text
|
|
199
|
-
* @returns {{ options: TocOptions, problem?: string }}
|
|
200
|
-
*/
|
|
201
|
-
function parseOptions(text) {
|
|
202
|
-
/** @type {TocOptions} */
|
|
203
|
-
const options = {};
|
|
204
|
-
/** @type {Record<string, string>} */
|
|
205
|
-
const attributes = {};
|
|
206
|
-
/** @type {string[]} */
|
|
207
|
-
const unknown = [];
|
|
208
|
-
for (const [token, name, value] of (text ?? '').matchAll(/([^\s="]+)(?:="([^"]*)")?(?=\s|$)|\S+/g)) {
|
|
209
|
-
if (name && KNOWN_OPTIONS.includes(name)) options[/** @type {'collapsible' | 'collapsed'} */ (name)] = value ?? true;
|
|
210
|
-
else if (name && value !== undefined && /^[a-zA-Z][\w:.-]*$/.test(name)) attributes[name] = value;
|
|
211
|
-
else unknown.push(token);
|
|
212
|
-
}
|
|
213
|
-
const names = Object.keys(attributes);
|
|
214
|
-
if (names.length) options.attributes = attributes;
|
|
215
|
-
if (unknown.length) return { options, problem: `unknown TOC option ${unknown.join(' ')}` };
|
|
216
|
-
if (options.collapsible !== undefined && options.collapsed !== undefined) {
|
|
217
|
-
return { options, problem: 'TOC options collapsible and collapsed exclude each other' };
|
|
218
|
-
}
|
|
219
|
-
if (names.length && options.collapsible === undefined && options.collapsed === undefined) {
|
|
220
|
-
return { options, problem: `TOC attributes ${names.join(', ')} need collapsible or collapsed` };
|
|
221
|
-
}
|
|
222
|
-
return { options };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* The start marker line for the given options, the inverse of `parseOptions`: options first, then attributes.
|
|
227
|
-
* @param {TocOptions} options
|
|
228
|
-
*/
|
|
229
|
-
function formatMarker(options) {
|
|
230
|
-
const { attributes = {}, ...named } = options;
|
|
231
|
-
const parts = [
|
|
232
|
-
...Object.entries(named).map(([name, value]) => (value === true ? name : `${name}="${value}"`)),
|
|
233
|
-
...formatAttributes(attributes),
|
|
234
|
-
];
|
|
235
|
-
return parts.length ? `<!-- toc ${parts.join(' ')} -->` : TOC_START;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* @param {Record<string, string>} attributes
|
|
240
|
-
* @returns {string[]} `name="value"` in the order given
|
|
241
|
-
*/
|
|
242
|
-
function formatAttributes(attributes) {
|
|
243
|
-
return Object.entries(attributes).map(([name, value]) => `${name}="${value}"`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* The label used in the toc: the heading's own markdown, except that links become their text since a link
|
|
248
|
-
* cannot nest inside the toc link.
|
|
249
|
-
* @param {string} markdown
|
|
250
|
-
* @returns {string}
|
|
251
|
-
*/
|
|
252
|
-
function headingLabel(markdown) {
|
|
253
|
-
const { text, restore } = protect(stripClosingHashes(markdown), { keepCodeSpans: true });
|
|
254
|
-
return restore(stripLinks(text)).trim();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Replace code spans (with their content, or the whole span when `keepCodeSpans` is set) and backslash escapes
|
|
259
|
-
* (with the escaped character) by placeholders so the inline markdown passes leave them alone.
|
|
260
|
-
* @param {string} markdown
|
|
261
|
-
* @param {{ keepCodeSpans?: boolean }} [options]
|
|
262
|
-
*/
|
|
263
|
-
function protect(markdown, { keepCodeSpans = false } = {}) {
|
|
264
|
-
/** @type {string[]} */
|
|
265
|
-
const kept = [];
|
|
266
|
-
/** @param {string} value */
|
|
267
|
-
const keep = (value) => `${NUL}${kept.push(value) - 1}${NUL}`;
|
|
268
|
-
const text = markdown
|
|
269
|
-
.replace(/(`+)(.+?)\1(?!`)/g, (span, _ticks, code) => keep(keepCodeSpans ? span : unpadCodeSpan(code)))
|
|
270
|
-
.replace(/\\([!-/:-@[-`{-~])/g, (_, char) => keep(char));
|
|
271
|
-
return {
|
|
272
|
-
text,
|
|
273
|
-
/** @param {string} value */
|
|
274
|
-
restore: (value) => value.replace(new RegExp(`${NUL}(\\d+)${NUL}`, 'g'), (_, i) => kept[Number(i)]),
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Strip one leading and one trailing space from code span content when both are present and the content is
|
|
280
|
-
* not only spaces, as CommonMark does.
|
|
281
|
-
* @param {string} code
|
|
282
|
-
*/
|
|
283
|
-
function unpadCodeSpan(code) {
|
|
284
|
-
if (code.length > 2 && code.startsWith(' ') && code.endsWith(' ') && code.trim() !== '') return code.slice(1, -1);
|
|
285
|
-
return code;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** @param {string} markdown */
|
|
289
|
-
function stripClosingHashes(markdown) {
|
|
290
|
-
return markdown.replace(/(^|\s+)#+\s*$/, '');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Links, images and reference links become their text, autolinks their url. Used for both the slug text and
|
|
295
|
-
* the toc label since a link cannot nest inside the toc link.
|
|
296
|
-
* @param {string} text
|
|
297
|
-
*/
|
|
298
|
-
function stripLinks(text) {
|
|
299
|
-
return text
|
|
300
|
-
.replace(/<(https?:\/\/[^>\s]+|mailto:[^>\s]+)>/g, '$1')
|
|
301
|
-
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
302
|
-
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
303
|
-
.replace(/\[([^\]]*)\]\[[^\]]*\]/g, '$1');
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** @param {string} line */
|
|
307
|
-
function isParagraphText(line) {
|
|
308
|
-
const trimmed = line.trim();
|
|
309
|
-
return trimmed !== '' && !/^(#|>|[-*+]\s|\d+[.)]\s|\||[-=]+\s*$|```|~~~|<!--)/.test(trimmed);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Options written on a start marker. `collapsible` wraps the list in a details element that starts open,
|
|
314
|
-
* `collapsed` in one that starts closed. Each is `true` for the default summary "Table of contents" or a
|
|
315
|
-
* string for a custom one. `attributes` are the other `name="value"` pairs on the marker, rendered on the
|
|
316
|
-
* summary element in the order written, only present when there are any.
|
|
317
|
-
* @typedef {{ collapsible?: true | string, collapsed?: true | string, attributes?: Record<string, string> }} TocOptions
|
|
318
|
-
*/
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* A marker pair. `start` and `end` are zero based line numbers, -1 when that side is missing. `problem` is
|
|
322
|
-
* only present when the start marker cannot be used: an unknown option or an impossible combination.
|
|
323
|
-
* @typedef {{ start: number, end: number, options: TocOptions, problem?: string }} Marker
|
|
324
|
-
*/
|
|
325
|
-
|
|
326
|
-
exports.TOC_END = TOC_END;
|
|
327
|
-
exports.TOC_START = TOC_START;
|
|
328
|
-
exports.buildToc = buildToc;
|
|
329
|
-
exports.findMarkers = findMarkers;
|
|
330
|
-
exports.headingText = headingText;
|
|
331
|
-
exports.renderToc = renderToc;
|
|
332
|
-
exports.slugify = slugify;
|