@datalackey/update-markdown-toc 1.1.9 → 1.1.11
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 +1 -1
- package/dist/cli/descriptor.d.ts +3 -0
- package/dist/cli/descriptor.js +40 -0
- package/dist/engine/TocFileProcessor.d.ts +5 -0
- package/dist/engine/TocFileProcessor.js +6 -0
- package/dist/engine/generateToc.d.ts +1 -0
- package/dist/engine/generateToc.js +48 -0
- package/dist/engine/processFile.d.ts +2 -0
- package/dist/engine/processFile.js +38 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -354,7 +354,7 @@ The intended workflow is:
|
|
|
354
354
|
|
|
355
355
|
This package is one component of a small ecosystem of JavaScript tooling plugins maintained as individual npm packages in this repository.
|
|
356
356
|
The versioning and release of these packages is governed by a coordinated release policy, and
|
|
357
|
-
the packages adhere to common
|
|
357
|
+
the packages adhere to common design and architectural principles policies
|
|
358
358
|
that are more completely described [here](../README.md).
|
|
359
359
|
|
|
360
360
|
## Known Limitations
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parseBooleanOption, parseNumberOption } from "@datalackey/tooling-core";
|
|
2
|
+
const DEFAULT_LINK_TIMEOUT_MS = 3000;
|
|
3
|
+
export const descriptor = {
|
|
4
|
+
name: "update-markdown-toc",
|
|
5
|
+
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 : standard.validateExternalLinks,
|
|
37
|
+
linkTimeoutMs: timeoutMs
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FileProcessor } from "@datalackey/tooling-core";
|
|
2
|
+
import type { RunConfig } from "@datalackey/tooling-core";
|
|
3
|
+
export declare class TocFileProcessor implements FileProcessor<RunConfig> {
|
|
4
|
+
process(filePath: string, config: RunConfig): import("@datalackey/tooling-core").ProcessingStatus;
|
|
5
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateTOC(content: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import GithubSlugger from "github-slugger";
|
|
2
|
+
const START = "<!-- TOC:START -->";
|
|
3
|
+
const END = "<!-- TOC:END -->";
|
|
4
|
+
function detectLineEnding(text) {
|
|
5
|
+
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
6
|
+
}
|
|
7
|
+
export function generateTOC(content) {
|
|
8
|
+
const lineEnding = detectLineEnding(content);
|
|
9
|
+
const hasStart = content.includes(START);
|
|
10
|
+
const hasEnd = content.includes(END);
|
|
11
|
+
if (!hasStart && !hasEnd)
|
|
12
|
+
throw new Error("TOC delimiters not found");
|
|
13
|
+
if (hasStart && !hasEnd)
|
|
14
|
+
throw new Error("TOC start delimiter found without end");
|
|
15
|
+
if (!hasStart && hasEnd)
|
|
16
|
+
throw new Error("TOC end delimiter found without start");
|
|
17
|
+
const startIndex = content.indexOf(START);
|
|
18
|
+
const endIndex = content.indexOf(END);
|
|
19
|
+
const before = content.slice(0, startIndex);
|
|
20
|
+
const after = content.slice(endIndex + END.length);
|
|
21
|
+
const contentWithoutTOC = before.replace(/\s*$/, "") +
|
|
22
|
+
lineEnding +
|
|
23
|
+
after.replace(/^\s*/, "");
|
|
24
|
+
const lines = contentWithoutTOC.split(lineEnding);
|
|
25
|
+
const headings = [];
|
|
26
|
+
const slugger = new GithubSlugger();
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const m = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
29
|
+
if (!m)
|
|
30
|
+
continue;
|
|
31
|
+
const level = m[1].length;
|
|
32
|
+
const title = m[2].trim();
|
|
33
|
+
const anchor = slugger.slug(title);
|
|
34
|
+
headings.push({ level: level, title: title, anchor: anchor });
|
|
35
|
+
}
|
|
36
|
+
if (headings.length === 0) {
|
|
37
|
+
throw new Error("No headings found to generate TOC");
|
|
38
|
+
}
|
|
39
|
+
const minLevel = Math.min(...headings.map((h) => h.level));
|
|
40
|
+
const tocLines = headings.map((h) => {
|
|
41
|
+
const indent = " ".repeat(h.level - minLevel);
|
|
42
|
+
return `${indent}- [${h.title}](#${h.anchor})`;
|
|
43
|
+
});
|
|
44
|
+
const tocBlock = lineEnding +
|
|
45
|
+
tocLines.join(lineEnding) +
|
|
46
|
+
lineEnding;
|
|
47
|
+
return before + START + tocBlock + END + after;
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { generateTOC } from "./generateToc.js";
|
|
4
|
+
import { debugLog } from "@datalackey/tooling-core";
|
|
5
|
+
export function processFile(filePath, config) {
|
|
6
|
+
const absolutePath = path.resolve(filePath);
|
|
7
|
+
debugLog(config, `processFile: entry filePath=${absolutePath} runMode=${config.runMode}`);
|
|
8
|
+
let content;
|
|
9
|
+
try {
|
|
10
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new Error(`Unable to read markdown file: ${absolutePath}`);
|
|
14
|
+
}
|
|
15
|
+
let updated;
|
|
16
|
+
try {
|
|
17
|
+
updated = generateTOC(content);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21
|
+
if (message === "TOC delimiters not found" && config.mode === "recursive") {
|
|
22
|
+
debugLog(config, `processFile: skipped (no markers) filePath=${absolutePath}`);
|
|
23
|
+
return "skipped";
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`${absolutePath}: ${message}`);
|
|
26
|
+
}
|
|
27
|
+
if (updated === content) {
|
|
28
|
+
debugLog(config, `processFile: unchanged filePath=${absolutePath}`);
|
|
29
|
+
return "unchanged";
|
|
30
|
+
}
|
|
31
|
+
if (config.runMode === "check") {
|
|
32
|
+
debugLog(config, `processFile: stale filePath=${absolutePath}`);
|
|
33
|
+
return "stale";
|
|
34
|
+
}
|
|
35
|
+
fs.writeFileSync(filePath, updated, "utf8");
|
|
36
|
+
debugLog(config, `processFile: updated filePath=${absolutePath}`);
|
|
37
|
+
return "updated";
|
|
38
|
+
}
|
package/package.json
CHANGED