@datalackey/update-markdown-toc 1.1.8 → 1.1.10

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 CHANGED
@@ -1,4 +1,4 @@
1
- # update-markdown-toc
1
+ # @datalackey/update-markdown-toc
2
2
 
3
3
  <p align="center">
4
4
  <img
@@ -10,7 +10,7 @@
10
10
 
11
11
 
12
12
  <!-- TOC:START -->
13
- - [update-markdown-toc](#update-markdown-toc)
13
+ - [@datalackey/update-markdown-toc](#datalackeyupdate-markdown-toc)
14
14
  - [Introduction](#introduction)
15
15
  - [Why not Some Other Markdown TOC Generator ?](#why-not-some-other-markdown-toc-generator-)
16
16
  - [Installation](#installation)
@@ -25,13 +25,17 @@
25
25
  - [Usage Scenarios](#usage-scenarios)
26
26
  - [As Part of code/test/debug Work Flow](#as-part-of-codetestdebug-work-flow)
27
27
  - [Continuous Integration](#continuous-integration)
28
+ - [Link Validation in Check Mode](#link-validation-in-check-mode)
28
29
  - [Recursively Traversing a Folder Hierarchy to Process all files vs. Single File Processing](#recursively-traversing-a-folder-hierarchy-to-process-all-files-vs-single-file-processing)
29
30
  - [Single-File Processing (Strict Mode)](#single-file-processing-strict-mode)
30
31
  - [Recursive Folder Traversal (Lenient Mode)](#recursive-folder-traversal-lenient-mode)
31
32
  - [Design Goals and Philosophy](#design-goals-and-philosophy)
33
+ - [Packaging, Publishing, and Inter-relationship with Other Plugins](#packaging-publishing-and-inter-relationship-with-other-plugins)
34
+ - [Known Limitations](#known-limitations)
32
35
  - [Guidelines For Project Contributors](#guidelines-for-project-contributors)
33
36
  <!-- TOC:END -->
34
37
 
38
+
35
39
  ## Introduction
36
40
 
37
41
  A Node.js command-line **documentation helper** which automatically:
@@ -121,16 +125,27 @@ update-markdown-toc [options] [file]
121
125
 
122
126
  Options:
123
127
  -c, --check <path-to-file-or-folder> Do not write files; exit non-zero if TOC is stale
128
+ -n, --no-external-link-check Skip external HTTP/HTTPS link validation during --check
129
+ -l, --link-timeout-ms <ms> Per-request timeout for external link checks (default: 3000)
124
130
  -r, --recursive <path-to-folder> Recursively process all .md files under the given folder
125
- -e, --exclude <dir1,dir2,...> Comma-separated list of directory names to exclude during recursive traversal (overrides default exclusion list)
131
+ -e, --exclude <dir1,dir2,...> Directory names to exclude during recursive traversal (overrides default)
126
132
  -v, --verbose Print status for every file processed
127
133
  -q, --quiet Suppress all non-error output
128
134
  -d, --debug Print debug diagnostics to stderr
129
135
  -h, --help Show this help message and exit
130
136
  ```
131
137
 
132
- When using --check, a target file or a recursive folder must be specified
133
- explicitly. Unlike normal operation, --check does not default to README.md.
138
+ When using `--check`, if no file is specified, the tool defaults to `README.md`
139
+ in the current working directory. This means the following two commands are equivalent:
140
+
141
+ ```bash
142
+ npx update-markdown-toc --check
143
+ npx update-markdown-toc --check README.md
144
+ ````
145
+
146
+ To check an entire documentation tree in CI, use `--check --recursive <path>.`
147
+
148
+
134
149
 
135
150
  ### Configurable Exclusion List for Recursive Traversal
136
151
 
@@ -226,8 +241,26 @@ Example:
226
241
  npx update-markdown-toc --check --recursive docs/
227
242
  ```
228
243
 
244
+ #### Link Validation in Check Mode
245
+
246
+ When running with `--check`, the tool performs three tiers of link validation in these scopes:
247
+ - table of contents links,
248
+ - intra-document links, and
249
+ - external links.
250
+
251
+ For full details on behavior, failure output, and performance considerations
252
+ see [Common CLI Behavior — Check Mode](../CLI-BEHAVIOR.md#
229
253
 
230
- If a pull request modifies documentation headings but forgets to update TOCs, this command will fail the build, forcing the contributor to regenerate and commit the correct TOC.
254
+
255
+ To skip external link validation:
256
+ ```bash
257
+ npx update-markdown-toc --check README.md --no-external-link-check
258
+ ```
259
+
260
+
261
+ If a pull request modifies documentation headings but forgets to update TOCs or other links targeted for
262
+ validation in a particular run, then the above command will fail the build,
263
+ forcing the contributor to regenerate and commit with corrected TOC and other links.
231
264
 
232
265
  ### Recursively Traversing a Folder Hierarchy to Process all files vs. Single File Processing
233
266
 
@@ -239,6 +272,9 @@ The tool supports two distinct operating modes with intentionally different erro
239
272
  These modes are designed to support both strict validation and incremental adoption across real-world repositories.
240
273
  In the case of the latter mode, we assume some files may not yet have TOC markers, and that this is acceptable.
241
274
 
275
+ Refer to [this document](../CLI-BEHAVIOR.md) for information on these processing modes and a discussion of other behavioral
276
+ commonalities that all focused-use plugins in this repository share.
277
+
242
278
  #### Single-File Processing (Strict Mode)
243
279
 
244
280
 
@@ -257,15 +293,33 @@ If either marker is missing, the tool prints an error message and exits with a n
257
293
 
258
294
  #### Recursive Folder Traversal (Lenient Mode)
259
295
 
260
- When operating in recursive mode, the tool traverses a directory tree and processes all *.md files it finds.
261
- In this mode, files without TOC markers are silently skipped, and files with TOC markers are processed normally.
262
- (Rational: for larger repos, it may not be feasible to add TOC markers to every Markdown file at once.)
263
- Stale files are reported (unless --quiet is specified) and will
264
- result in a non-zero return value at the end of processing, but the
265
- script will not immediately bail with an error -- processing continues for all files found.
296
+ When operating in recursive mode, the tool traverses a directory tree and processes all `*.md` files it finds.
297
+
298
+ In this mode:
299
+
300
+ - Files without TOC markers are skipped silently (unless `--verbose` is specified).
301
+ - Files with valid TOC markers are processed normally.
302
+ - Stale files are reported (unless `--quiet` is specified).
303
+ - When running in `--check` mode, stale files cause a non-zero exit code after traversal completes.
304
+
305
+ Recursive mode is designed for gradual adoption across larger repositories, where not every Markdown file may yet contain TOC markers.
306
+ Unlike single-file mode, recursive mode does **not** treat missing TOC markers as an error. This allows incremental rollout of TOC enforcement.
307
+
308
+ However, structural or filesystem errors still abort the run immediately. These include:
309
+
310
+ - unreadable files (e.g., permission errors),
311
+ - mismatched TOC delimiters,
312
+ - malformed TOC marker pairs,
313
+ - files containing TOC markers but no Markdown headings.
314
+
315
+ When such errors occur, the tool prints an error message and exits non-zero without continuing further traversal.
316
+
317
+ When combined with `--verbose`, skipped files (Markdown files without start/end region markers) are reported explicitly. For example:
318
+
319
+ ```bash
320
+ update-markdown-toc --recursive docs/ --verbose
321
+
266
322
 
267
- When combined with --verbose, skipped files (Markdown files without start/end region markers)
268
- are reported explicitly in this mode. For example:
269
323
 
270
324
  ```bash
271
325
  update-markdown-toc --recursive docs/ --verbose
@@ -295,8 +349,27 @@ The intended workflow is:
295
349
  3. CI runs in `--check` mode to ensure no drift exists.
296
350
 
297
351
 
298
- ## Guidelines For Project Contributors
299
352
 
300
- Contributors to the project should consult [this document](GUIDELINES-FOR-PROJECT-CONTRIBUTORS.md)
353
+ ## Packaging, Publishing, and Inter-relationship with Other Plugins
354
+
355
+ This package is one component of a small ecosystem of JavaScript tooling plugins maintained as individual npm packages in this repository.
356
+ The versioning and release of these packages is governed by a coordinated release policy, and
357
+ the packages adhere to common architectural policies (ESM-only, Node 18+, strict TypeScript configuration)
358
+ that are more completely described [here](../README.md).
359
+
360
+ ## Known Limitations
361
+
362
+ **Fragment link validation with formatted headings**
363
+
364
+ Fragment link validation uses AST-based heading extraction, which may produce
365
+ different slugs than the TOC generator for headings containing inline formatting
366
+ such as code spans or bold text (e.g. `## Install \`foo\``).
367
+
368
+ In practice this affects only headings with inline code, bold, or italic syntax.
369
+ Plain-text headings are unaffected. A fix to unify both paths is planned for a
370
+ future release.
371
+
372
+ ## Guidelines For Project Contributors
301
373
 
374
+ Contributors to the project should consult [this document](docs/CONTRIBUTING.md)
302
375
 
@@ -1,57 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseCli } from "../src/cli/parseCli.js";
4
- import { runSingleFile, runRecursive } from "../src/index.js";
5
- import { resolve } from "node:path";
6
- import { existsSync, statSync } from "node:fs";
7
-
8
- function printHelp() {
9
- console.log(`
10
- update-markdown-toc [options] [file]
11
-
12
- Options:
13
- -c, --check
14
- -r, --recursive <path>
15
- -e, --exclude <dir1,dir2,...>
16
- -v, --verbose
17
- -q, --quiet
18
- -d, --debug
19
- -h, --help
20
- `);
21
- }
22
-
23
- try {
24
- const config = parseCli(process.argv.slice(2));
25
-
26
- if (config.help) {
27
- printHelp();
28
- process.exit(0);
29
- }
30
-
31
- if (config.isRecursive) {
32
- const resolved = resolve(process.cwd(), config.recursivePath);
33
-
34
- if (!existsSync(resolved)) {
35
- console.error("ERROR: Recursive path does not exist");
36
- process.exit(1);
37
- }
38
-
39
- if (!statSync(resolved).isDirectory()) {
40
- console.error("ERROR: --recursive requires a directory");
41
- process.exit(1);
42
- }
43
-
44
- process.exit(runRecursive(resolved, config));
45
- }
46
-
47
- const resolved = resolve(
48
- process.cwd(),
49
- config.targetFile || "README.md"
50
- );
51
-
52
- process.exit(runSingleFile(resolved, config));
53
-
54
- } catch (err) {
55
- console.error(`ERROR: ${err.message}`);
56
- process.exit(1);
57
- }
3
+ import { runCli } from "@datalackey/tooling-core"
4
+ import { TocFileProcessor } from "../dist/engine/TocFileProcessor.js"
5
+ import { descriptor } from "../dist/cli/descriptor.js"
6
+
7
+ await runCli({
8
+ descriptor: descriptor,
9
+ processor: new TocFileProcessor()
10
+ })
@@ -0,0 +1,3 @@
1
+ import type { RunConfig } from "@datalackey/tooling-core";
2
+ import type { PluginDescriptor } from "@datalackey/tooling-core";
3
+ export declare const descriptor: PluginDescriptor<RunConfig>;
@@ -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,6 @@
1
+ import { processFile } from "./processFile.js";
2
+ export class TocFileProcessor {
3
+ process(filePath, config) {
4
+ return processFile(filePath, config);
5
+ }
6
+ }
@@ -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,2 @@
1
+ import type { RunConfig, ProcessingStatus } from "@datalackey/tooling-core";
2
+ export declare function processFile(filePath: string, config: RunConfig): ProcessingStatus;
@@ -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
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@datalackey/update-markdown-toc",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
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
- "test": "bash scripts/run-all-tests.sh",
10
- "prepack": "npm run test",
11
- "test:unit": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js tests/unit"
9
+ "build": "tsc -p tsconfig.json",
10
+ "test": "bash scripts/run-all-tests.sh"
12
11
  },
13
12
  "bin": {
14
13
  "update-markdown-toc": "./bin/update-markdown-toc.js"
15
14
  },
16
15
  "files": [
17
- "bin/"
16
+ "bin/",
17
+ "dist/"
18
18
  ],
19
19
  "engines": {
20
20
  "node": ">=18"
@@ -44,10 +44,13 @@
44
44
  },
45
45
  "homepage": "https://github.com/datalackey/build-tools/tree/main/javascript/update-markdown-toc",
46
46
  "dependencies": {
47
+ "@datalackey/tooling-core": "*",
47
48
  "github-slugger": "^2.0.0",
48
49
  "ts-dedent": "^2.2.0"
49
50
  },
50
51
  "devDependencies": {
51
- "jest": "^30.2.0"
52
+ "@types/node": "^18.19.130",
53
+ "jest": "^30.2.0",
54
+ "typescript": "^5.9.3"
52
55
  }
53
- }
56
+ }