From 2c964e85a8bf7c07a693a9bedc0cf7714373d0b2 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 26 Jun 2026 18:31:12 +1000 Subject: [PATCH 1/6] refactor content generation for testability --- README.md | 2 -- lib/content-generation.js | 37 ++++++++++++++++++++++++++++++++++++- lib/transform.js | 29 +++++++++++------------------ 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7f587a6b..21378c43 100644 --- a/README.md +++ b/README.md @@ -241,8 +241,6 @@ It is recommended to also include this argument when performing an update on an Use the `--toc-title-padding-before` option to add padding line/s above the TOC which ensures formatters such as prettier will pass; e.g., `doctoc --toc-title-padding-before 1 .` -NOTE: Currently it is only supported to add one line before the title. - By default, - no padding is added above the table of contents title diff --git a/lib/content-generation.js b/lib/content-generation.js index da692c3a..f8872ac7 100644 --- a/lib/content-generation.js +++ b/lib/content-generation.js @@ -55,10 +55,45 @@ function escapedStartTag(syntax) { return commentTypes[syntax].escapedStart; } +function tocFooter(footerOptions) { + if (footerOptions.content === undefined) return []; + var lines = Array(footerOptions.padding?.before ?? 0).fill(''); + lines.push(footerOptions.content); + lines.push(...Array(footerOptions.padding?.after ?? 0).fill('')); + return lines; +} + +function tocHeader(headerOptions) { + if (headerOptions.content === undefined) return []; + var lines = Array(headerOptions.padding?.before ?? 0).fill(''); + lines.push(headerOptions.content); + lines.push(...Array(headerOptions.padding?.after ?? 0).fill('')); + return lines; +} + +function tocTitle(titleOptions, existingTitle) { + if (titleOptions.remove) return []; + var lines = Array(titleOptions.padding?.before ?? 0).fill(''); + if (titleOptions.content){ + lines.push(titleOptions.content); + } + else if(existingTitle){ + lines.push(existingTitle); + } + else{ + lines.push('**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*'); + } + lines.push(...Array(titleOptions.padding?.after ?? 0).fill('')); + return lines; +} + module.exports = { commentedBlock, escapedStartTag, pragmaMarkers, skipTag, - excludeTag + excludeTag, + tocFooter, + tocHeader, + tocTitle }; diff --git a/lib/transform.js b/lib/transform.js index 95b2c52e..8d6cbf7e 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -148,12 +148,8 @@ function processHeaders(headers, mode) { // When updating an existing TOC, try to preserve the title that was there before. // Falls back to CLI args (--title, --notitle) or the default title for new TOCs. -function determineTitle(nodes, tocPosition, title, notitle, header) { - var defaultTitle = '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*'; - - if (notitle) return ""; - if (title) return title; - if (!tocPosition?.existing) return defaultTitle; +function determineTitle(nodes, tocPosition, titleOptions, header) { + if (titleOptions.remove || titleOptions.content || !tocPosition?.existing) return undefined; // Find the TOC's list node so we know where the title region ends var listStartIdx = nodes.children.filter(function (x) { @@ -170,8 +166,8 @@ function determineTitle(nodes, tocPosition, title, notitle, header) { var title = paragraphs?.[paragraphs.length - 1]?.raw; // If the inferred title matches --toc-header-content, suppress it to avoid duplication - if (title && header?.includes(title)) return ""; - return title || ""; + if (title && header?.includes(title)) return undefined; + return title || undefined; } function detectLineEnding(content, defaultEol) { @@ -199,6 +195,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min options.toc ??= {}; options.toc.items ??= {}; options.toc.items.indentation ??= {}; + options.toc.title ??= {}; syntax = syntax || "md"; options.toc.items.indentation.style ??= 'space'; @@ -206,6 +203,8 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min mode === 'bitbucket.org' || mode === 'gitlab.com' ? 4 : 2 ); options.toc.items.symbols ??= String(entryPrefix || '-').split(','); + options.toc.title.content ??= title; + options.toc.title.remove ??= notitle; var eol = '\n'; eol = detectLineEnding(content, eol); @@ -220,11 +219,6 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min minTocItems = 1; } - var padTitle = options?.toc?.title?.padding?.before > 0; - if (options?.toc?.title?.padding?.before === undefined){ - padTitle = notitle || false; - } - // only limit *HTML* headings by default var maxHeaderLevelHtml = maxHeaderLevel || 4; @@ -303,16 +297,15 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min if (headers.length >= minTocItems && docNodes.children[docNodes.children.length - 1].loc.end.line >= minLines) { var tocLines = []; - var inferredTitle = determineTitle(docNodes, tocPosition, title, notitle, options?.toc?.header?.content); + var inferredTitle = determineTitle(docNodes, tocPosition, options.toc.title, options.toc.header?.content); var allHeaders = processHeaders(headers, mode); var lowestRank = allHeaders.reduce((min, h) => Math.min(min, h.rank), Infinity); var indentation = options.toc.items.indentation.style === 'tab' ? '\t' : ' '; - if (options?.toc?.header?.content) { tocLines.push(options.toc.header.content); } - if (padTitle && inferredTitle) { tocLines.push(''); } - if (inferredTitle) { tocLines.push(inferredTitle); } + tocLines.push(...contentGenerator.tocHeader(options.toc.header)); + tocLines.push(...contentGenerator.tocTitle(options.toc.title, inferredTitle)); tocLines.push(''); var symbolQty = options.toc.items.symbols.length; @@ -325,7 +318,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min })); tocLines.push(''); - if (options?.toc?.footer?.content) { tocLines.push(options.toc.footer.content); } + tocLines.push(...contentGenerator.tocFooter(options.toc.footer)); toc = tocLines.join(eol); } From fbeb8a58038167bbd32ff3df1f046d373ca87fb5 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 26 Jun 2026 18:49:27 +1000 Subject: [PATCH 2/6] Fixes for tests --- lib/content-generation.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/content-generation.js b/lib/content-generation.js index f8872ac7..110713e4 100644 --- a/lib/content-generation.js +++ b/lib/content-generation.js @@ -56,25 +56,25 @@ function escapedStartTag(syntax) { } function tocFooter(footerOptions) { - if (footerOptions.content === undefined) return []; - var lines = Array(footerOptions.padding?.before ?? 0).fill(''); + if (footerOptions?.content === undefined) return []; + var lines = Array(footerOptions?.padding?.before ?? 0).fill(''); lines.push(footerOptions.content); - lines.push(...Array(footerOptions.padding?.after ?? 0).fill('')); + lines.push(...Array(footerOptions?.padding?.after ?? 0).fill('')); return lines; } function tocHeader(headerOptions) { - if (headerOptions.content === undefined) return []; - var lines = Array(headerOptions.padding?.before ?? 0).fill(''); + if (headerOptions?.content === undefined) return []; + var lines = Array(headerOptions?.padding?.before ?? 0).fill(''); lines.push(headerOptions.content); - lines.push(...Array(headerOptions.padding?.after ?? 0).fill('')); + lines.push(...Array(headerOptions?.padding?.after ?? 0).fill('')); return lines; } function tocTitle(titleOptions, existingTitle) { - if (titleOptions.remove) return []; - var lines = Array(titleOptions.padding?.before ?? 0).fill(''); - if (titleOptions.content){ + if (titleOptions?.remove) return []; + var lines = Array(titleOptions?.padding?.before ?? 0).fill(''); + if (titleOptions?.content){ lines.push(titleOptions.content); } else if(existingTitle){ @@ -83,7 +83,7 @@ function tocTitle(titleOptions, existingTitle) { else{ lines.push('**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*'); } - lines.push(...Array(titleOptions.padding?.after ?? 0).fill('')); + lines.push(...Array(titleOptions?.padding?.after ?? 0).fill('')); return lines; } From 2bb7867b12320bb8165642a063ecc914564ce4a6 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 26 Jun 2026 19:06:47 +1000 Subject: [PATCH 3/6] Support existing doc with no toc --- lib/transform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 8d6cbf7e..61c88ace 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -166,8 +166,8 @@ function determineTitle(nodes, tocPosition, titleOptions, header) { var title = paragraphs?.[paragraphs.length - 1]?.raw; // If the inferred title matches --toc-header-content, suppress it to avoid duplication - if (title && header?.includes(title)) return undefined; - return title || undefined; + if (title && header?.includes(title)) return ""; + return title || ""; } function detectLineEnding(content, defaultEol) { From b3d947a589f27e44f363976cc2f897911b4a87d6 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 26 Jun 2026 19:17:11 +1000 Subject: [PATCH 4/6] Handle no existing title --- lib/content-generation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/content-generation.js b/lib/content-generation.js index 110713e4..1b8f5a86 100644 --- a/lib/content-generation.js +++ b/lib/content-generation.js @@ -72,7 +72,7 @@ function tocHeader(headerOptions) { } function tocTitle(titleOptions, existingTitle) { - if (titleOptions?.remove) return []; + if (titleOptions?.remove || (existingTitle === "" && !titleOptions?.content)) return []; var lines = Array(titleOptions?.padding?.before ?? 0).fill(''); if (titleOptions?.content){ lines.push(titleOptions.content); From 4426341713e27c0ffa250e9a3ef45f4574b1006c Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 26 Jun 2026 19:55:37 +1000 Subject: [PATCH 5/6] Optimise --- lib/content-generation.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/content-generation.js b/lib/content-generation.js index 1b8f5a86..cc524f47 100644 --- a/lib/content-generation.js +++ b/lib/content-generation.js @@ -56,34 +56,33 @@ function escapedStartTag(syntax) { } function tocFooter(footerOptions) { - if (footerOptions?.content === undefined) return []; - var lines = Array(footerOptions?.padding?.before ?? 0).fill(''); - lines.push(footerOptions.content); - lines.push(...Array(footerOptions?.padding?.after ?? 0).fill('')); - return lines; + return tocPart(footerOptions?.padding, footerOptions?.content); } function tocHeader(headerOptions) { - if (headerOptions?.content === undefined) return []; - var lines = Array(headerOptions?.padding?.before ?? 0).fill(''); - lines.push(headerOptions.content); - lines.push(...Array(headerOptions?.padding?.after ?? 0).fill('')); - return lines; + return tocPart(headerOptions?.padding, headerOptions?.content); } function tocTitle(titleOptions, existingTitle) { if (titleOptions?.remove || (existingTitle === "" && !titleOptions?.content)) return []; - var lines = Array(titleOptions?.padding?.before ?? 0).fill(''); + var content; if (titleOptions?.content){ - lines.push(titleOptions.content); + content = titleOptions.content; } else if(existingTitle){ - lines.push(existingTitle); + content = existingTitle; } else{ - lines.push('**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*'); + content = '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*'; } - lines.push(...Array(titleOptions?.padding?.after ?? 0).fill('')); + return tocPart(titleOptions?.padding, content); +} + +function tocPart(padding, content){ + if (content === undefined) return []; + var lines = Array(padding?.before ?? 0).fill(''); + lines.push(content); + lines.push(...Array(padding?.after ?? 0).fill('')); return lines; } From 080da6b97bc56b86479013ae9261145d9a8c1da3 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Mon, 29 Jun 2026 13:26:09 +1000 Subject: [PATCH 6/6] review feedback --- lib/content-generation.js | 4 ++-- lib/transform.js | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/content-generation.js b/lib/content-generation.js index cc524f47..549c5f97 100644 --- a/lib/content-generation.js +++ b/lib/content-generation.js @@ -80,9 +80,9 @@ function tocTitle(titleOptions, existingTitle) { function tocPart(padding, content){ if (content === undefined) return []; - var lines = Array(padding?.before ?? 0).fill(''); + var lines = Array(Number(padding?.before ?? 0)).fill(''); lines.push(content); - lines.push(...Array(padding?.after ?? 0).fill('')); + lines.push(...Array(Number(padding?.after ?? 0)).fill('')); return lines; } diff --git a/lib/transform.js b/lib/transform.js index e7ce11db..d957d891 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -153,8 +153,6 @@ function processHeaders(headers, mode) { return headers; } -// When updating an existing TOC, try to preserve the title that was there before. -// Falls back to CLI args (--title, --notitle) or the default title for new TOCs. function determineTitle(nodes, tocPosition, titleOptions, header) { if (titleOptions.remove || titleOptions.content || !tocPosition?.existing) return undefined;