@datalackey/update-markdown-toc 1.3.0 → 1.4.1

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
@@ -366,7 +366,8 @@ future release.
366
366
 
367
367
  ## Contributing and Releasing
368
368
 
369
- 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
370
370
  trigger a publish via Changesets), see
371
- [CONTRIBUTING.md](../docs/CONTRIBUTING.md).
371
+ [CONTRIBUTING.md](./docs/CONTRIBUTING.md).
372
+
372
373
 
@@ -1 +1,2 @@
1
+ export declare function stripInlineCode(text: string): string;
1
2
  export declare function generateTOC(content: string): string;
@@ -1,28 +1,19 @@
1
- import GithubSlugger from "github-slugger";
1
+ import { parseHeadings } from "@datalackey/tooling-core";
2
2
  const START = "<!-- TOC:START -->";
3
3
  const END = "<!-- TOC:END -->";
4
- function stripFencedLines(lines) {
5
- let inFence = false;
6
- const result = [];
7
- for (const line of lines) {
8
- if (line.startsWith("```")) {
9
- inFence = !inFence;
10
- continue;
11
- }
12
- if (!inFence)
13
- result.push(line);
14
- }
15
- if (inFence)
16
- throw new Error("Unclosed code fence (```) in document");
17
- return result;
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, "");
18
8
  }
19
9
  function detectLineEnding(text) {
20
10
  return text.includes("\r\n") ? "\r\n" : "\n";
21
11
  }
22
12
  export function generateTOC(content) {
23
13
  const lineEnding = detectLineEnding(content);
24
- const hasStart = content.includes(START);
25
- const hasEnd = content.includes(END);
14
+ const stripped = stripInlineCode(content);
15
+ const hasStart = stripped.includes(START);
16
+ const hasEnd = stripped.includes(END);
26
17
  if (!hasStart && !hasEnd)
27
18
  throw new Error("TOC delimiters not found");
28
19
  if (hasStart && !hasEnd)
@@ -33,26 +24,29 @@ export function generateTOC(content) {
33
24
  const endIndex = content.indexOf(END);
34
25
  const before = content.slice(0, startIndex);
35
26
  const after = content.slice(endIndex + END.length);
36
- const contentWithoutTOC = before.replace(/\s*$/, "") + lineEnding + after.replace(/^\s*/, "");
37
- const lines = contentWithoutTOC.split(lineEnding);
38
- const headings = [];
39
- const slugger = new GithubSlugger();
40
- for (const line of stripFencedLines(lines)) {
41
- const m = /^(#{1,6})\s+(.*)$/.exec(line);
42
- if (!m)
43
- continue;
44
- const level = m[1].length;
45
- const title = m[2].trim();
46
- const anchor = slugger.slug(title);
47
- headings.push({ level: level, title: title, anchor: anchor });
48
- }
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] ?? ""));
49
43
  if (headings.length === 0) {
50
44
  throw new Error("No headings found to generate TOC");
51
45
  }
52
46
  const minLevel = Math.min(...headings.map((h) => h.level));
53
47
  const tocLines = headings.map((h) => {
54
48
  const indent = " ".repeat(h.level - minLevel);
55
- return `${indent}- [${h.title}](#${h.anchor})`;
49
+ return `${indent}- [${h.rawText}](#${h.slug})`;
56
50
  });
57
51
  const tocBlock = lineEnding + tocLines.join(lineEnding) + lineEnding;
58
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 = err instanceof Error ? err.message : String(err);
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.0",
3
+ "version": "1.4.1",
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
- "build": "tsc -p tsconfig.json",
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"