@datalackey/update-markdown-toc 0.1.3
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 +211 -0
- package/bin/parseCli.js +93 -0
- package/bin/update-markdown-toc.js +263 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# update-markdown-toc
|
|
2
|
+
|
|
3
|
+
- [update-markdown-toc](#update-markdown-toc)
|
|
4
|
+
- [Introduction](#introduction)
|
|
5
|
+
- [Installation](#installation)
|
|
6
|
+
- [Usage](#usage)
|
|
7
|
+
- [Using npx (recommended)](#using-npx-recommended)
|
|
8
|
+
- [Using npm scripts](#using-npm-scripts)
|
|
9
|
+
- [Using a direct path (advanced)](#using-a-direct-path-advanced)
|
|
10
|
+
- [Options](#options)
|
|
11
|
+
- [TOC Markers](#toc-markers)
|
|
12
|
+
- [Usage Scenarios](#usage-scenarios)
|
|
13
|
+
- [As Part of code/test/debug Work Flow](#as-part-of-codetestdebug-work-flow)
|
|
14
|
+
- [Continuous Integration (CI)](#continuous-integration--ci)
|
|
15
|
+
- [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)
|
|
16
|
+
- [Single-File Processing (Strict Mode)](#single-file-processing-strict-mode)
|
|
17
|
+
- [Recursive Folder Traversal (Lenient Mode)](#recursive-folder-traversal-lenient-mode)
|
|
18
|
+
- [Guidelines For Project Contributors](#guidelines-for-project-contributors)
|
|
19
|
+
|
|
20
|
+
## Introduction
|
|
21
|
+
|
|
22
|
+
A Node.js command-line **documentation helper** which automatically:
|
|
23
|
+
|
|
24
|
+
- generates Table of Contents (TOC) blocks for Markdown files
|
|
25
|
+
- operates on either a single file, or recursively finds all `*.md` files from a root path
|
|
26
|
+
- regenerates TOCs from headings, replacing only explicitly marked regions, with no gratuitous reformatting
|
|
27
|
+
- avoids updating files when the generated TOC is already correct
|
|
28
|
+
- provides a `--check` mode which flags Markdown files with stale TOCs (intended for CI)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Install as a development dependency (recommended):
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install --save-dev @datalackey/update-markdown-toc
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This installs the update-markdown-toc command into your project’s
|
|
42
|
+
node_modules/.bin/ directory.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
After installation, the `update-markdown-toc` command can be invoked in any
|
|
50
|
+
of the following ways from the project root (or a subdirectory) where the package was installed.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Using npx (recommended)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx update-markdown-toc README.md
|
|
57
|
+
````
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
### Using npm scripts
|
|
61
|
+
|
|
62
|
+
You may also add a script entry to your package.json:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"scripts": {
|
|
67
|
+
"docs:toc": "update-markdown-toc README.md"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
Then run:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm run docs:toc
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Using a direct path (advanced)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
./node_modules/.bin/update-markdown-toc README.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Options
|
|
86
|
+
|
|
87
|
+
This section assumes the command is invoked using `npx`, an npm script,
|
|
88
|
+
or another method that resolves the local `update-markdown-toc` binary.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
update-markdown-toc [options] [file]
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
-c, --check <path-to-file-or-folder> Do not write files; exit non-zero if TOC is stale
|
|
96
|
+
-r, --recursive <path-to-folder> Recursively process all .md files under the given folder
|
|
97
|
+
-v, --verbose Print status for every file processed
|
|
98
|
+
-q, --quiet Suppress all non-error output
|
|
99
|
+
-d, --debug Print debug diagnostics to stderr
|
|
100
|
+
-h, --help Show this help message and exit
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## TOC Markers
|
|
104
|
+
|
|
105
|
+
The tool operates only on files containing **both** markers:
|
|
106
|
+
|
|
107
|
+
```md
|
|
108
|
+
<!-- TOC:START -->
|
|
109
|
+
<!-- TOC:END -->
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Any existing content between these markers is lost. The new content will be the generated TOC that
|
|
113
|
+
reflects the section headers marked with '#'s in the Markdown document.
|
|
114
|
+
|
|
115
|
+
Content outside the markers is preserved verbatim.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## Usage Scenarios
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
### As Part of code/test/debug Work Flow
|
|
124
|
+
|
|
125
|
+
To ensure that your code is built afresh, passes tests, and that your documentation TOCs are up to date, you could
|
|
126
|
+
use invoke the tool in something akin to the package.json below.
|
|
127
|
+
Before commit and push, you would type: 'npm run build'
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
Your `package.json` might look like this:
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"scripts": {
|
|
134
|
+
"clean": "rm -rf dist",
|
|
135
|
+
"compile": "tsc -p tsconfig.json",
|
|
136
|
+
"pretest": "npm run compile",
|
|
137
|
+
"test": "jest",
|
|
138
|
+
"docs:toc": "update-markdown-toc --recursive docs/",
|
|
139
|
+
"bundle": "esbuild src/index.ts --bundle --platform=node --outdir=dist",
|
|
140
|
+
"package": "npm run clean && npm run compile && npm run bundle",
|
|
141
|
+
"build": "npm run docs:toc && npm run test && npm run package"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Continuous Integration (CI)
|
|
147
|
+
|
|
148
|
+
The --check flag is designed primarily for continuous integration.
|
|
149
|
+
|
|
150
|
+
In this mode, the tool:
|
|
151
|
+
|
|
152
|
+
- never writes files
|
|
153
|
+
- compares the existing TOC block against the generated TOC
|
|
154
|
+
- exits with a non-zero status if any TOC is stale
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npx update-markdown-toc --check --recursive docs/
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
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.
|
|
165
|
+
|
|
166
|
+
### Recursively Traversing a Folder Hierarchy to Process all files vs. Single File Processing
|
|
167
|
+
|
|
168
|
+
The tool supports two distinct operating modes with intentionally different error-handling semantics:
|
|
169
|
+
|
|
170
|
+
- Single-file mode (--recursive not specified)
|
|
171
|
+
- Recursive folder traversal mode (--recursive specified)
|
|
172
|
+
|
|
173
|
+
These modes are designed to support both strict validation and incremental adoption across real-world repositories.
|
|
174
|
+
In the case of the latter mode, we assume some files may not yet have TOC markers, and that this is acceptable.
|
|
175
|
+
|
|
176
|
+
#### Single-File Processing (Strict Mode)
|
|
177
|
+
|
|
178
|
+
When a single Markdown file is explicitly specified (or when the default README.md is used), the tool operates in strict mode.
|
|
179
|
+
|
|
180
|
+
In this mode:
|
|
181
|
+
|
|
182
|
+
The file must contain both TOC markers:
|
|
183
|
+
```md
|
|
184
|
+
<!-- TOC:START -->
|
|
185
|
+
<!-- TOC:END -->
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If either marker is missing, the tool prints an error message and exits with a non-zero status code.
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
#### Recursive Folder Traversal (Lenient Mode)
|
|
193
|
+
|
|
194
|
+
When operating in recursive mode, the tool traverses a directory tree and processes all *.md files it finds.
|
|
195
|
+
In this mode, files without TOC markers are silently skipped, and files with TOC markers are processed normally.
|
|
196
|
+
|
|
197
|
+
When combined with --verbose, skipped files are reported explicitly in this mode.
|
|
198
|
+
|
|
199
|
+
update-markdown-toc --recursive docs/ --verbose
|
|
200
|
+
|
|
201
|
+
Example output:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
Skipped (no markers): docs/legacy-notes.md
|
|
205
|
+
Updated: docs/guide.md
|
|
206
|
+
Up-to-date: docs/api.md
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Guidelines For Project Contributors
|
|
210
|
+
|
|
211
|
+
Contributors to the project should consult [this document](GUIDELINES-FOR-PROJECT-CONTRIBUTORS.md)
|
package/bin/parseCli.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { parseCli } from "./parseCli.js";
|
|
6
|
+
|
|
7
|
+
/* ============================================================
|
|
8
|
+
* Constants
|
|
9
|
+
* ============================================================ */
|
|
10
|
+
|
|
11
|
+
const START = "<!-- TOC:START -->";
|
|
12
|
+
const END = "<!-- TOC:END -->";
|
|
13
|
+
|
|
14
|
+
/* ============================================================
|
|
15
|
+
* CLI configuration
|
|
16
|
+
* ============================================================ */
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
checkMode,
|
|
20
|
+
verbose,
|
|
21
|
+
quiet,
|
|
22
|
+
debug,
|
|
23
|
+
recursivePath,
|
|
24
|
+
targetFile,
|
|
25
|
+
isRecursive
|
|
26
|
+
} = parseCli();
|
|
27
|
+
|
|
28
|
+
/* ============================================================
|
|
29
|
+
* Debug helper
|
|
30
|
+
* ============================================================ */
|
|
31
|
+
|
|
32
|
+
function debugLog(msg) {
|
|
33
|
+
if (debug) {
|
|
34
|
+
console.error(`[debug] ${msg}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ============================================================
|
|
39
|
+
* Helpers
|
|
40
|
+
* ============================================================ */
|
|
41
|
+
|
|
42
|
+
function collectMarkdownFiles(dir) {
|
|
43
|
+
const results = [];
|
|
44
|
+
|
|
45
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
46
|
+
const full = path.join(dir, entry.name);
|
|
47
|
+
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
results.push(...collectMarkdownFiles(full));
|
|
50
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
51
|
+
results.push(full);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function generateTOC(content) {
|
|
59
|
+
const hasStart = content.includes(START);
|
|
60
|
+
const hasEnd = content.includes(END);
|
|
61
|
+
|
|
62
|
+
if (!hasStart && !hasEnd) {
|
|
63
|
+
throw new Error("TOC delimiters not found");
|
|
64
|
+
}
|
|
65
|
+
if (hasStart && !hasEnd) {
|
|
66
|
+
throw new Error("TOC start delimiter found without end");
|
|
67
|
+
}
|
|
68
|
+
if (!hasStart && hasEnd) {
|
|
69
|
+
throw new Error("TOC end delimiter found without start");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const startIndex = content.indexOf(START);
|
|
73
|
+
const endIndex = content.indexOf(END);
|
|
74
|
+
|
|
75
|
+
const before = content.slice(0, startIndex + START.length);
|
|
76
|
+
const after = content.slice(endIndex);
|
|
77
|
+
|
|
78
|
+
const contentWithoutTOC =
|
|
79
|
+
content.slice(0, startIndex) +
|
|
80
|
+
content.slice(endIndex + END.length);
|
|
81
|
+
|
|
82
|
+
const lines = contentWithoutTOC.split("\n");
|
|
83
|
+
const headings = [];
|
|
84
|
+
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const m = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
87
|
+
if (!m) continue;
|
|
88
|
+
|
|
89
|
+
const level = m[1].length;
|
|
90
|
+
const title = m[2].trim();
|
|
91
|
+
|
|
92
|
+
const anchor = title
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.replace(/[^\w\s-]/g, "")
|
|
95
|
+
.replace(/\s/g, "-")
|
|
96
|
+
.replace(/^-|-$/g, "");
|
|
97
|
+
|
|
98
|
+
headings.push({ level, title, anchor });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (headings.length === 0) {
|
|
102
|
+
throw new Error("No headings found to generate TOC");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const minLevel = Math.min(...headings.map(h => h.level));
|
|
106
|
+
const tocLines = headings.map(h => {
|
|
107
|
+
const indent = " ".repeat(h.level - minLevel);
|
|
108
|
+
return `${indent}- [${h.title}](#${h.anchor})`;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const tocBlock = "\n" + tocLines.join("\n") + "\n";
|
|
112
|
+
return before + tocBlock + after;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ============================================================
|
|
116
|
+
* File processing
|
|
117
|
+
* ============================================================ */
|
|
118
|
+
|
|
119
|
+
function processFile(filePath) {
|
|
120
|
+
debugLog(`processing file: ${filePath}`);
|
|
121
|
+
|
|
122
|
+
let content;
|
|
123
|
+
try {
|
|
124
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
125
|
+
} catch {
|
|
126
|
+
throw new Error(`Unable to read markdown file: ${filePath}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let updated;
|
|
130
|
+
try {
|
|
131
|
+
updated = generateTOC(content);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err.message === "TOC delimiters not found") {
|
|
134
|
+
if (isRecursive) {
|
|
135
|
+
debugLog("result: skipped (no markers)");
|
|
136
|
+
return { status: "skipped" };
|
|
137
|
+
}
|
|
138
|
+
throw err; // single-file mode → hard error
|
|
139
|
+
}
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (updated === content) {
|
|
144
|
+
debugLog("result: unchanged");
|
|
145
|
+
return { status: "unchanged" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (checkMode) {
|
|
149
|
+
debugLog("result: stale");
|
|
150
|
+
return { status: "stale" };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fs.writeFileSync(filePath, updated, "utf8");
|
|
154
|
+
debugLog("result: updated");
|
|
155
|
+
return { status: "updated" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* ============================================================
|
|
159
|
+
* Output
|
|
160
|
+
* ============================================================ */
|
|
161
|
+
|
|
162
|
+
function maybePrintStatus(status, filePath) {
|
|
163
|
+
debugLog(`printing decision: status=${status}`);
|
|
164
|
+
|
|
165
|
+
if (quiet) return;
|
|
166
|
+
|
|
167
|
+
if (checkMode) {
|
|
168
|
+
// In --check mode we ALWAYS report stale files (unless --quiet).
|
|
169
|
+
// This makes CI output actionable without requiring --verbose.
|
|
170
|
+
if (status === "stale") {
|
|
171
|
+
console.log(`Stale: ${filePath}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// In recursive mode, files without markers are intentionally ignored.
|
|
176
|
+
// They should not cause failures and do not need reporting by default.
|
|
177
|
+
if (status === "skipped") {
|
|
178
|
+
if (verbose) {
|
|
179
|
+
console.log(`Skipped (no markers): ${filePath}`);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (status === "unchanged") {
|
|
185
|
+
if (verbose) {
|
|
186
|
+
console.log(`Up-to-date: ${filePath}`);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (verbose) {
|
|
195
|
+
if (status === "updated") {
|
|
196
|
+
console.log(`Updated: ${filePath}`);
|
|
197
|
+
} else if (status === "unchanged") {
|
|
198
|
+
console.log(`Up-to-date: ${filePath}`);
|
|
199
|
+
} else if (status === "skipped") {
|
|
200
|
+
console.log(`Skipped (no markers): ${filePath}`);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (status === "updated") {
|
|
206
|
+
console.log(`Updated: ${filePath}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
/* ============================================================
|
|
212
|
+
* Execution
|
|
213
|
+
* ============================================================ */
|
|
214
|
+
|
|
215
|
+
let files = [];
|
|
216
|
+
|
|
217
|
+
if (recursivePath) {
|
|
218
|
+
const resolved = path.resolve(process.cwd(), recursivePath);
|
|
219
|
+
|
|
220
|
+
if (!fs.existsSync(resolved)) {
|
|
221
|
+
console.error("ERROR: Recursive path does not exist");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
if (!fs.statSync(resolved).isDirectory()) {
|
|
225
|
+
console.error("ERROR: --recursive requires a directory");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
files = collectMarkdownFiles(resolved);
|
|
230
|
+
files.sort(); // deterministic order
|
|
231
|
+
} else {
|
|
232
|
+
const resolved = path.resolve(
|
|
233
|
+
process.cwd(),
|
|
234
|
+
targetFile || "README.md"
|
|
235
|
+
);
|
|
236
|
+
files = [resolved];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let staleFound = false;
|
|
240
|
+
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
try {
|
|
243
|
+
const result = processFile(file);
|
|
244
|
+
|
|
245
|
+
if (checkMode && result.status === "stale") {
|
|
246
|
+
staleFound = true;
|
|
247
|
+
debugLog("staleFound set true");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
maybePrintStatus(result.status, file);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error(`ERROR: ${err.message}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (checkMode && staleFound) {
|
|
258
|
+
debugLog("exiting with status 1 due to stale TOC");
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
debugLog("exiting with status 0");
|
|
263
|
+
process.exit(0);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@datalackey/update-markdown-toc",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Auto-generate Table of Contents for a Markdown file (or files, recursively from a top level folder)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"update-markdown-toc": "./bin/update-markdown-toc.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"build",
|
|
18
|
+
"automation",
|
|
19
|
+
"javascript",
|
|
20
|
+
"node",
|
|
21
|
+
"documentation",
|
|
22
|
+
"readme",
|
|
23
|
+
"toc",
|
|
24
|
+
"markdown",
|
|
25
|
+
"ci"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/datalackey/build-tools.git",
|
|
33
|
+
"directory": "javascript/update-markdown-toc"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/datalackey/build-tools/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/datalackey/build-tools/tree/main/javascript/update-markdown-toc",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"ts-dedent": "^2.2.0"
|
|
41
|
+
}
|
|
42
|
+
}
|