@datalackey/update-markdown-toc 1.1.5 → 1.1.6

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
@@ -19,6 +19,8 @@
19
19
  - [Using npm scripts](#using-npm-scripts)
20
20
  - [Using a direct path (advanced)](#using-a-direct-path-advanced)
21
21
  - [Options](#options)
22
+ - [Configurable Exclusion List for Recursive Traversal](#configurable-exclusion-list-for-recursive-traversal)
23
+ - [Examples:](#examples)
22
24
  - [TOC Markers](#toc-markers)
23
25
  - [Usage Scenarios](#usage-scenarios)
24
26
  - [As Part of code/test/debug Work Flow](#as-part-of-codetestdebug-work-flow)
@@ -28,7 +30,6 @@
28
30
  - [Recursive Folder Traversal (Lenient Mode)](#recursive-folder-traversal-lenient-mode)
29
31
  - [Design Goals and Philosophy](#design-goals-and-philosophy)
30
32
  - [Guidelines For Project Contributors](#guidelines-for-project-contributors)
31
- - [Known limitations](#known-limitations)
32
33
  <!-- TOC:END -->
33
34
 
34
35
  ## Introduction
@@ -120,6 +121,7 @@ update-markdown-toc [options] [file]
120
121
  Options:
121
122
  -c, --check <path-to-file-or-folder> Do not write files; exit non-zero if TOC is stale
122
123
  -r, --recursive <path-to-folder> Recursively process all .md files under the given folder
124
+ -e, --exclude <dir1,dir2,...> Comma-separated list of directory names to exclude during recursive traversal (overrides default exclusion list)
123
125
  -v, --verbose Print status for every file processed
124
126
  -q, --quiet Suppress all non-error output
125
127
  -d, --debug Print debug diagnostics to stderr
@@ -129,6 +131,37 @@ Options:
129
131
  When using --check, a target file or a recursive folder must be specified
130
132
  explicitly. Unlike normal operation, --check does not default to README.md.
131
133
 
134
+ ### Configurable Exclusion List for Recursive Traversal
135
+
136
+ The `--exclude` option only applies when `--recursive` is specified.
137
+ It accepts a comma-separated list of directory names (exact match, not glob patterns), and
138
+ when provided, it replaces the default exclusion list.
139
+
140
+ By default, the recursive traversal excludes only: `node_modules`.
141
+
142
+ #### Examples:
143
+
144
+ Exclude node_modules and dist:
145
+
146
+ ```bash
147
+ npx update-markdown-toc --recursive . --exclude node_modules,dist
148
+ ```
149
+
150
+ Exclude only dist (this allows traversal into node_modules):
151
+
152
+ ```bash
153
+ npx update-markdown-toc --recursive . --exclude dist
154
+ ```
155
+
156
+ Disable exclusions entirely:
157
+
158
+ ```bash
159
+ npx update-markdown-toc --recursive . --exclude ""
160
+ ```
161
+
162
+
163
+
164
+
132
165
 
133
166
  ## TOC Markers
134
167
 
@@ -228,7 +261,7 @@ In this mode, files without TOC markers are silently skipped, and files with TOC
228
261
  (Rational: for larger repos, it may not be feasible to add TOC markers to every Markdown file at once.)
229
262
  Stale files are reported (unless --quiet is specified) and will
230
263
  result in a non-zero return value at the end of processing, but the
231
- script will not immediately bail wiht an error -- processing continues for all files found.
264
+ script will not immediately bail with an error -- processing continues for all files found.
232
265
 
233
266
  When combined with --verbose, skipped files (Markdown files without start/end region markers)
234
267
  are reported explicitly in this mode. For example:
@@ -266,8 +299,3 @@ The intended workflow is:
266
299
  Contributors to the project should consult [this document](GUIDELINES-FOR-PROJECT-CONTRIBUTORS.md)
267
300
 
268
301
 
269
- ## Known limitations
270
-
271
- - node_modules is excluded from recursive traversal: when using `--recursive` the tool will skip any directory
272
- named `node_modules` and its contents. This prevents accidental processing or modification of third-party files.
273
- This exclusion is currently hard-coded and not configurable via command-line flags or configuration files.
@@ -1,279 +1,57 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import fs from "fs";
4
- import path from "path";
5
- import { parseCli } from "./parseCli.js";
6
- import GithubSlugger from "github-slugger";
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
7
 
8
- /* ============================================================
9
- * Constants
10
- * ============================================================ */
8
+ function printHelp() {
9
+ console.log(`
10
+ update-markdown-toc [options] [file]
11
11
 
12
- const START = "<!-- TOC:START -->";
13
- const END = "<!-- TOC:END -->";
14
-
15
- /* ============================================================
16
- * CLI configuration
17
- * ============================================================ */
18
-
19
- const {
20
- checkMode,
21
- verbose,
22
- quiet,
23
- debug,
24
- recursivePath,
25
- targetFile,
26
- isRecursive
27
- } = parseCli();
28
-
29
- /* ============================================================
30
- * Debug helper
31
- * ============================================================ */
32
-
33
- function debugLog(msg) {
34
- if (debug) {
35
- console.error(`[debug] ${msg}`);
36
- }
37
- }
38
-
39
- /* ============================================================
40
- * Helpers
41
- * ============================================================ */
42
-
43
- function detectLineEnding(text) {
44
- return text.includes("\r\n") ? "\r\n" : "\n";
45
- }
46
-
47
- function collectMarkdownFiles(dir) {
48
- const results = [];
49
-
50
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
51
- const full = path.join(dir, entry.name);
52
-
53
- if (entry.isDirectory()) {
54
- // Exclude node_modules from recursive traversal to avoid processing third-party files
55
- if (entry.name === 'node_modules') continue;
56
-
57
- results.push(...collectMarkdownFiles(full));
58
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
59
- results.push(full);
60
- }
61
- }
62
-
63
- return results;
64
- }
65
-
66
- function generateTOC(content) {
67
- const lineEnding = detectLineEnding(content);
68
-
69
- const hasStart = content.includes(START);
70
- const hasEnd = content.includes(END);
71
-
72
- if (!hasStart && !hasEnd) {
73
- throw new Error("TOC delimiters not found");
74
- }
75
- if (hasStart && !hasEnd) {
76
- throw new Error("TOC start delimiter found without end");
77
- }
78
- if (!hasStart && hasEnd) {
79
- throw new Error("TOC end delimiter found without start");
80
- }
81
-
82
- const startIndex = content.indexOf(START);
83
- const endIndex = content.indexOf(END);
84
-
85
- const before = content.slice(0, startIndex);
86
- const after = content.slice(endIndex + END.length);
87
-
88
- // Preserve exactly one line boundary for parsing
89
- const contentWithoutTOC =
90
- before.replace(/\s*$/, "") +
91
- lineEnding +
92
- after.replace(/^\s*/, "");
93
-
94
- const lines = contentWithoutTOC.split(lineEnding);
95
- const headings = [];
96
-
97
- const slugger = new GithubSlugger();
98
-
99
- for (const line of lines) {
100
- const m = /^(#{1,6})\s+(.*)$/.exec(line);
101
- if (!m) continue;
102
-
103
- const level = m[1].length;
104
- const title = m[2].trim();
105
- const anchor = slugger.slug(title);
106
-
107
- headings.push({ level, title, anchor });
108
- }
109
-
110
- if (headings.length === 0) {
111
- throw new Error("No headings found to generate TOC");
112
- }
113
-
114
- const minLevel = Math.min(...headings.map(h => h.level));
115
- const tocLines = headings.map(h => {
116
- const indent = " ".repeat(h.level - minLevel);
117
- return `${indent}- [${h.title}](#${h.anchor})`;
118
- });
119
-
120
- const tocBlock =
121
- lineEnding +
122
- tocLines.join(lineEnding) +
123
- lineEnding;
124
-
125
- return (
126
- before +
127
- START +
128
- tocBlock +
129
- END +
130
- after
131
- );
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
+ `);
132
21
  }
133
22
 
134
- /* ============================================================
135
- * File processing
136
- * ============================================================ */
137
-
138
- function processFile(filePath) {
139
- debugLog(`processing file: ${filePath}`);
140
-
141
- let content;
142
- try {
143
- content = fs.readFileSync(filePath, "utf8");
144
- } catch {
145
- const absolutePath = path.resolve(filePath);
146
- throw new Error(`Unable to read markdown file: ${absolutePath}`);
147
- }
148
-
149
- let updated;
150
- try {
151
- updated = generateTOC(content);
152
- } catch (err) {
153
- if (err.message === "TOC delimiters not found") {
154
- if (isRecursive) {
155
- debugLog("result: skipped (no markers)");
156
- return { status: "skipped" };
157
- }
158
- }
23
+ try {
24
+ const config = parseCli(process.argv.slice(2));
159
25
 
160
- const absolutePath = path.resolve(filePath);
161
- throw new Error(`${absolutePath}: ${err.message}`);
26
+ if (config.help) {
27
+ printHelp();
28
+ process.exit(0);
162
29
  }
163
30
 
164
- if (updated === content) {
165
- debugLog("result: unchanged");
166
- return { status: "unchanged" };
167
- }
31
+ if (config.isRecursive) {
32
+ const resolved = resolve(process.cwd(), config.recursivePath);
168
33
 
169
- if (checkMode) {
170
- debugLog("result: stale");
171
- return { status: "stale" };
172
- }
173
-
174
- fs.writeFileSync(filePath, updated, "utf8");
175
- debugLog("result: updated");
176
- return { status: "updated" };
177
- }
178
-
179
- /* ============================================================
180
- * Output
181
- * ============================================================ */
182
-
183
- function maybePrintStatus(status, filePath) {
184
- debugLog(`printing decision: status=${status}`);
185
-
186
- if (quiet) return;
187
-
188
- if (checkMode) {
189
- if (status === "stale") {
190
- console.log(`Stale: ${filePath}`);
191
- return;
34
+ if (!existsSync(resolved)) {
35
+ console.error("ERROR: Recursive path does not exist");
36
+ process.exit(1);
192
37
  }
193
38
 
194
- if (status === "skipped") {
195
- if (verbose) {
196
- console.log(`Skipped (no markers): ${filePath}`);
197
- }
198
- return;
39
+ if (!statSync(resolved).isDirectory()) {
40
+ console.error("ERROR: --recursive requires a directory");
41
+ process.exit(1);
199
42
  }
200
43
 
201
- if (status === "unchanged") {
202
- if (verbose) {
203
- console.log(`Up-to-date: ${filePath}`);
204
- }
205
- return;
206
- }
207
-
208
- return;
209
- }
210
-
211
- if (verbose) {
212
- if (status === "updated") {
213
- console.log(`Updated: ${filePath}`);
214
- } else if (status === "unchanged") {
215
- console.log(`Up-to-date: ${filePath}`);
216
- } else if (status === "skipped") {
217
- console.log(`Skipped (no markers): ${filePath}`);
218
- }
219
- return;
44
+ process.exit(runRecursive(resolved, config));
220
45
  }
221
46
 
222
- if (status === "updated") {
223
- console.log(`Updated: ${filePath}`);
224
- }
225
- }
226
-
227
- /* ============================================================
228
- * Execution
229
- * ============================================================ */
230
-
231
- let files = [];
232
-
233
- if (recursivePath) {
234
- const resolved = path.resolve(process.cwd(), recursivePath);
235
-
236
- if (!fs.existsSync(resolved)) {
237
- console.error("ERROR: Recursive path does not exist");
238
- process.exit(1);
239
- }
240
- if (!fs.statSync(resolved).isDirectory()) {
241
- console.error("ERROR: --recursive requires a directory");
242
- process.exit(1);
243
- }
244
-
245
- files = collectMarkdownFiles(resolved);
246
- files.sort();
247
- } else {
248
- const resolved = path.resolve(
47
+ const resolved = resolve(
249
48
  process.cwd(),
250
- targetFile || "README.md"
49
+ config.targetFile || "README.md"
251
50
  );
252
- files = [resolved];
253
- }
254
-
255
- let staleFound = false;
256
51
 
257
- for (const file of files) {
258
- try {
259
- const result = processFile(file);
52
+ process.exit(runSingleFile(resolved, config));
260
53
 
261
- if (checkMode && result.status === "stale") {
262
- staleFound = true;
263
- debugLog("staleFound set true");
264
- }
265
-
266
- maybePrintStatus(result.status, file);
267
- } catch (err) {
268
- console.error(`ERROR: ${err.message}`);
269
- process.exit(1);
270
- }
271
- }
272
-
273
- if (checkMode && staleFound) {
274
- debugLog("exiting with status 1 due to stale TOC");
54
+ } catch (err) {
55
+ console.error(`ERROR: ${err.message}`);
275
56
  process.exit(1);
276
- }
277
-
278
- debugLog("exiting with status 0");
279
- process.exit(0);
57
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@datalackey/update-markdown-toc",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
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
9
  "test": "bash scripts/run-all-tests.sh",
10
- "prepack": "npm run test"
10
+ "prepack": "npm run test",
11
+ "test:unit": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js tests/unit"
11
12
  },
12
13
  "bin": {
13
14
  "update-markdown-toc": "./bin/update-markdown-toc.js"
@@ -45,5 +46,8 @@
45
46
  "dependencies": {
46
47
  "github-slugger": "^2.0.0",
47
48
  "ts-dedent": "^2.2.0"
49
+ },
50
+ "devDependencies": {
51
+ "jest": "^30.2.0"
48
52
  }
49
- }
53
+ }
package/bin/parseCli.js DELETED
@@ -1,97 +0,0 @@
1
- import { parseArgs } from "node:util";
2
- import { dedent } from "ts-dedent";
3
-
4
- export function parseCli() {
5
- let values, positionals;
6
-
7
- try {
8
- ({ values, positionals } = parseArgs({
9
- options: {
10
- check: { type: "boolean", short: "c" },
11
- recursive: { type: "string", short: "r" },
12
- verbose: { type: "boolean", short: "v" },
13
- quiet: { type: "boolean", short: "q" },
14
- debug: { type: "boolean", short: "d" },
15
- help: { type: "boolean", short: "h" }
16
- },
17
- allowShort: true,
18
- allowPositionals: true
19
- }));
20
- } catch (err) {
21
- console.error(`ERROR: ${err.message}`);
22
- process.exit(1);
23
- }
24
-
25
- if (values.help) {
26
- printHelp();
27
- process.exit(0);
28
- }
29
-
30
- const checkMode = values.check === true;
31
- const verbose = values.verbose === true;
32
- const quiet = values.quiet === true;
33
- const debug = values.debug === true;
34
-
35
- const recursivePath =
36
- typeof values.recursive === "string" ? values.recursive : null;
37
-
38
- let targetFile = null;
39
-
40
- if (positionals.length > 1) {
41
- console.error("ERROR: Only one file argument may be provided");
42
- process.exit(1);
43
- }
44
-
45
- if (positionals.length === 1) {
46
- targetFile = positionals[0];
47
- }
48
-
49
- // -------------------------
50
- // Contract validation
51
- // -------------------------
52
-
53
- if (quiet && verbose) {
54
- console.error("ERROR: --quiet and --verbose cannot be used together");
55
- process.exit(1);
56
- }
57
-
58
- if (checkMode && !recursivePath && !targetFile) {
59
- console.error("ERROR: --check requires a file or --recursive <path>");
60
- process.exit(1);
61
- }
62
-
63
- if (recursivePath && targetFile) {
64
- console.error("ERROR: Cannot use --recursive with a file argument");
65
- process.exit(1);
66
- }
67
-
68
- return Object.freeze({
69
- checkMode,
70
- verbose,
71
- quiet,
72
- debug,
73
-
74
- recursivePath,
75
- targetFile,
76
-
77
- isRecursive: Boolean(recursivePath)
78
- });
79
- }
80
-
81
- function printHelp() {
82
- console.log(dedent`
83
- update-markdown-toc [options] [file]
84
-
85
- Options:
86
- -c, --check <path-to-file-or-folder> Do not write files; exit non-zero if TOC is stale
87
- -r, --recursive <path-to-folder> Recursively process all .md files under the given folder
88
- -v, --verbose Print status for every file processed
89
- -q, --quiet Suppress all non-error output
90
- -d, --debug Print debug diagnostics to stderr
91
- -h, --help Show this help message and exit
92
-
93
- When using --check, a target file or a recursive folder must be specified
94
- explicitly. Unlike normal operation, --check does not default to README.md.
95
-
96
- `);
97
- }