@datalackey/update-markdown-toc 1.2.1 → 1.4.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/README.md +3 -7
- package/dist/cli/descriptor.d.ts +1 -2
- package/dist/cli/descriptor.js +3 -41
- package/dist/engine/generateToc.d.ts +1 -0
- package/dist/engine/generateToc.js +26 -17
- package/dist/engine/processFile.js +2 -2
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -316,11 +316,6 @@ When such errors occur, the tool prints an error message and exits non-zero with
|
|
|
316
316
|
|
|
317
317
|
When combined with `--verbose`, skipped files (Markdown files without start/end region markers) are reported explicitly. For example:
|
|
318
318
|
|
|
319
|
-
```bash
|
|
320
|
-
update-markdown-toc --recursive docs/ --verbose
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
319
|
```bash
|
|
325
320
|
update-markdown-toc --recursive docs/ --verbose
|
|
326
321
|
```
|
|
@@ -371,7 +366,8 @@ future release.
|
|
|
371
366
|
|
|
372
367
|
## Contributing and Releasing
|
|
373
368
|
|
|
374
|
-
For development setup, build workflow, and release procedures (including how to
|
|
369
|
+
For code overview, development setup, build workflow, and release procedures (including how to
|
|
375
370
|
trigger a publish via Changesets), see
|
|
376
|
-
[CONTRIBUTING.md](
|
|
371
|
+
[CONTRIBUTING.md](./docs/CONTRIBUTING.md).
|
|
372
|
+
|
|
377
373
|
|
package/dist/cli/descriptor.d.ts
CHANGED
package/dist/cli/descriptor.js
CHANGED
|
@@ -1,50 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const DEFAULT_LINK_TIMEOUT_MS = 3000;
|
|
1
|
+
import { runLinkValidation } from "@datalackey/tooling-core";
|
|
3
2
|
export const descriptor = {
|
|
4
3
|
name: "update-markdown-toc",
|
|
5
4
|
description: "Auto-generate Table of Contents for Markdown files",
|
|
6
|
-
options: [
|
|
7
|
-
{
|
|
8
|
-
flag: "--no-external-link-check",
|
|
9
|
-
description: "Skip external link validation in check mode",
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
flag: "-n",
|
|
13
|
-
description: "Skip external link validation in check mode (short form)",
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
flag: "--link-timeout-ms",
|
|
17
|
-
description: "Timeout in milliseconds for external link requests (default: 3000)",
|
|
18
|
-
requiresValue: true,
|
|
19
|
-
valueName: "ms",
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
flag: "-l",
|
|
23
|
-
description: "Timeout in milliseconds for external link requests (short form)",
|
|
24
|
-
requiresValue: true,
|
|
25
|
-
valueName: "ms",
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
parseOptions(standard, passthrough) {
|
|
29
|
-
const noExternalCheck = parseBooleanOption("--no-external-link-check", passthrough) ||
|
|
30
|
-
parseBooleanOption("-n", passthrough);
|
|
31
|
-
const timeoutMs = parseNumberOption("--link-timeout-ms", passthrough) ??
|
|
32
|
-
parseNumberOption("-l", passthrough) ??
|
|
33
|
-
DEFAULT_LINK_TIMEOUT_MS;
|
|
34
|
-
return {
|
|
35
|
-
...standard,
|
|
36
|
-
validateExternalLinks: noExternalCheck ? false : true,
|
|
37
|
-
linkTimeoutMs: timeoutMs,
|
|
38
|
-
};
|
|
39
|
-
},
|
|
5
|
+
options: [],
|
|
40
6
|
async afterRun(files, config) {
|
|
41
7
|
if (config.runMode !== "check") {
|
|
42
8
|
return;
|
|
43
9
|
}
|
|
44
|
-
await runLinkValidation(files,
|
|
45
|
-
...config,
|
|
46
|
-
validateExternalLinks: config.validateExternalLinks,
|
|
47
|
-
linkTimeoutMs: config.linkTimeoutMs,
|
|
48
|
-
});
|
|
10
|
+
await runLinkValidation(files, config);
|
|
49
11
|
},
|
|
50
12
|
};
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseHeadings } from "@datalackey/tooling-core";
|
|
2
2
|
const START = "<!-- TOC:START -->";
|
|
3
3
|
const END = "<!-- TOC:END -->";
|
|
4
|
+
export function stripInlineCode(text) {
|
|
5
|
+
// [^`\n]* — any char except backtick or newline (inline spans can't cross lines)
|
|
6
|
+
// Replace with "" not "``" — substituting backticks reintroduces spans that eat surrounding text
|
|
7
|
+
return text.replace(/`[^`\n]*`/g, "");
|
|
8
|
+
}
|
|
4
9
|
function detectLineEnding(text) {
|
|
5
10
|
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
6
11
|
}
|
|
7
12
|
export function generateTOC(content) {
|
|
8
13
|
const lineEnding = detectLineEnding(content);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
14
|
+
const stripped = stripInlineCode(content);
|
|
15
|
+
const hasStart = stripped.includes(START);
|
|
16
|
+
const hasEnd = stripped.includes(END);
|
|
11
17
|
if (!hasStart && !hasEnd)
|
|
12
18
|
throw new Error("TOC delimiters not found");
|
|
13
19
|
if (hasStart && !hasEnd)
|
|
@@ -18,26 +24,29 @@ export function generateTOC(content) {
|
|
|
18
24
|
const endIndex = content.indexOf(END);
|
|
19
25
|
const before = content.slice(0, startIndex);
|
|
20
26
|
const after = content.slice(endIndex + END.length);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
// Two line endings (a blank line) are required between before and after so
|
|
28
|
+
// that any open HTML block in `before` (e.g. a closing </p> tag with no
|
|
29
|
+
// trailing blank line) is closed before remark parses `after`. A single \n
|
|
30
|
+
// is not enough — CommonMark only closes an HTML block at a blank line.
|
|
31
|
+
const contentWithoutTOC = before.replace(/\s*$/, "") +
|
|
32
|
+
lineEnding +
|
|
33
|
+
lineEnding +
|
|
34
|
+
after.replace(/^\s*/, "");
|
|
35
|
+
// parseHeadings (remark/CommonMark) also returns setext-style headings — text
|
|
36
|
+
// immediately followed by `---` or `===` on the next line. This is valid
|
|
37
|
+
// CommonMark but users writing `paragraph\n---` as a paragraph + horizontal
|
|
38
|
+
// rule (a very common pattern) will unintentionally produce TOC entries.
|
|
39
|
+
// We restrict to ATX-style headings (lines starting with `#`) which are the
|
|
40
|
+
// only form a user would deliberately put in a TOC.
|
|
41
|
+
const sourceLines = contentWithoutTOC.split(lineEnding);
|
|
42
|
+
const headings = parseHeadings(contentWithoutTOC).filter((h) => h.line > 0 && /^#{1,6}\s/.test(sourceLines[h.line - 1] ?? ""));
|
|
34
43
|
if (headings.length === 0) {
|
|
35
44
|
throw new Error("No headings found to generate TOC");
|
|
36
45
|
}
|
|
37
46
|
const minLevel = Math.min(...headings.map((h) => h.level));
|
|
38
47
|
const tocLines = headings.map((h) => {
|
|
39
48
|
const indent = " ".repeat(h.level - minLevel);
|
|
40
|
-
return `${indent}- [${h.
|
|
49
|
+
return `${indent}- [${h.rawText}](#${h.slug})`;
|
|
41
50
|
});
|
|
42
51
|
const tocBlock = lineEnding + tocLines.join(lineEnding) + lineEnding;
|
|
43
52
|
return before + START + tocBlock + END + after;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { generateTOC } from "./generateToc.js";
|
|
4
|
-
import { debugLog } from "@datalackey/tooling-core";
|
|
4
|
+
import { debugLog, toErrorMessage } from "@datalackey/tooling-core";
|
|
5
5
|
export function processFile(filePath, config) {
|
|
6
6
|
const absolutePath = path.resolve(filePath);
|
|
7
7
|
debugLog(config, `processFile: entry filePath=${absolutePath} runMode=${config.runMode}`);
|
|
@@ -17,7 +17,7 @@ export function processFile(filePath, config) {
|
|
|
17
17
|
updated = generateTOC(content);
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
|
20
|
-
const message =
|
|
20
|
+
const message = toErrorMessage(err);
|
|
21
21
|
if (message === "TOC delimiters not found" && config.mode === "recursive") {
|
|
22
22
|
debugLog(config, `processFile: skipped (no markers) filePath=${absolutePath}`);
|
|
23
23
|
return "skipped";
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalackey/update-markdown-toc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Auto-generate Table of Contents for a Markdown file (or files, recursively from a top level folder)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"private": false,
|
|
8
8
|
"scripts": {
|
|
9
|
-
"
|
|
10
|
-
"test": "npx vitest run --config vitest.config.ts && bash scripts/run-all-tests.sh"
|
|
9
|
+
"prepack": "npx nx build @datalackey/update-markdown-toc 1>&2"
|
|
11
10
|
},
|
|
12
11
|
"bin": {
|
|
13
12
|
"update-markdown-toc": "./bin/update-markdown-toc.js"
|