From d84408130828b9a042d847add7c9ed28130f2c8a Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 17 Apr 2026 02:06:27 +1000 Subject: [PATCH 01/26] Scaffold #279 --- lib/transform.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 02df2f0a..d2a410ee 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -182,13 +182,17 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min var docNodes = md.parse(content); var tocs = getTocMarkers(docNodes, options?.toc?.pragma?.style, syntax); - if (!(tocs.positions?.length > 0) && updateOnly) { return { transformed: false }; } + + var startPos = 0; + if (docNodes.children[0].type == 'Yaml') { + startPos = docNodes.children[1].range[0]; + } if (!(tocs.positions?.length > 0)) { - var insertPos = 0; + var insertPos = startPos; tocs.positions = [{ end: { range: [insertPos, insertPos], @@ -203,7 +207,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min } var tocPosition = tocs.positions[0]; - var tocableStart = !processAll ? tocPosition.end.range[1] : 0; + var tocableStart = !processAll ? tocPosition.end.range[1] : startPos; var headers = getMarkdownHeaders(docNodes, tocableStart, maxHeaderLevel, minHeaderLevel) .concat(getHtmlHeaders(docNodes, tocableStart, maxHeaderLevelHtml, minHeaderLevel)); From 599d5c25acae83a3b481e5eae1555b2c625221e1 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 17 Apr 2026 12:30:34 +1000 Subject: [PATCH 02/26] Add tests for front matter --- lib/transform.js | 6 ++ test/fixtures/readme-frontmatter-invalid.md | 7 ++ test/fixtures/readme-frontmatter-json.md | 7 ++ test/fixtures/readme-frontmatter-toml.md | 7 ++ test/fixtures/readme-frontmatter-yaml.md | 7 ++ test/transform-frontmatter.js | 113 ++++++++++++++++++++ 6 files changed, 147 insertions(+) create mode 100644 test/fixtures/readme-frontmatter-invalid.md create mode 100644 test/fixtures/readme-frontmatter-json.md create mode 100644 test/fixtures/readme-frontmatter-toml.md create mode 100644 test/fixtures/readme-frontmatter-yaml.md create mode 100644 test/transform-frontmatter.js diff --git a/lib/transform.js b/lib/transform.js index d2a410ee..4af4b012 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -190,6 +190,12 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min if (docNodes.children[0].type == 'Yaml') { startPos = docNodes.children[1].range[0]; } + else if(docNodes.children[0].type == 'Paragraph' && docNodes.children[0].raw.startsWith('+++') && docNodes.children[0].raw.endsWith('+++')){ + startPos = docNodes.children[1].range[0]; + } + else if(docNodes.children[0].type == 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.endsWith(';;;')){ + startPos = docNodes.children[1].range[0]; + } if (!(tocs.positions?.length > 0)) { var insertPos = startPos; diff --git a/test/fixtures/readme-frontmatter-invalid.md b/test/fixtures/readme-frontmatter-invalid.md new file mode 100644 index 00000000..b4cf2894 --- /dev/null +++ b/test/fixtures/readme-frontmatter-invalid.md @@ -0,0 +1,7 @@ ++++ +title: Invalid Front Matter +||| + +# Heading + +Your regular Markdown content follows... \ No newline at end of file diff --git a/test/fixtures/readme-frontmatter-json.md b/test/fixtures/readme-frontmatter-json.md new file mode 100644 index 00000000..a9b872a4 --- /dev/null +++ b/test/fixtures/readme-frontmatter-json.md @@ -0,0 +1,7 @@ +;;; +title: JSON Front Matter +;;; + +# Heading + +Your regular Markdown content follows... \ No newline at end of file diff --git a/test/fixtures/readme-frontmatter-toml.md b/test/fixtures/readme-frontmatter-toml.md new file mode 100644 index 00000000..a3131f6c --- /dev/null +++ b/test/fixtures/readme-frontmatter-toml.md @@ -0,0 +1,7 @@ ++++ +title: TOML Front Matter ++++ + +# Heading + +Your regular Markdown content follows... \ No newline at end of file diff --git a/test/fixtures/readme-frontmatter-yaml.md b/test/fixtures/readme-frontmatter-yaml.md new file mode 100644 index 00000000..72f98b2c --- /dev/null +++ b/test/fixtures/readme-frontmatter-yaml.md @@ -0,0 +1,7 @@ +--- +title: Yaml Front Matter +--- + +# Heading + +Your regular Markdown content follows... \ No newline at end of file diff --git a/test/transform-frontmatter.js b/test/transform-frontmatter.js new file mode 100644 index 00000000..00e0de00 --- /dev/null +++ b/test/transform-frontmatter.js @@ -0,0 +1,113 @@ +'use strict'; +/*jshint asi: true */ + +var test = require('tap').test + , transform = require('../lib/transform') + +test('\ngiven a file that includes Json frontmatter', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-json.md', 'utf8'); + var res = transform(content); + + t.same( + res.data + , [ ';;;', + 'title: JSON Front Matter', + ';;;', + '', + '', + '', + '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*', + '', + '- [Heading](#heading)', + '', + '', + '', + '# Heading', + '', + 'Your regular Markdown content follows...' ].join('\n') + , 'generates correct toc for file with Json frontmatter' + ) + + t.end() +}) + +test('\ngiven a file that includes Toml frontmatter', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-toml.md', 'utf8'); + var res = transform(content); + + t.same( + res.data + , [ '+++', + 'title: TOML Front Matter', + '+++', + '', + '', + '', + '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*', + '', + '- [Heading](#heading)', + '', + '', + '', + '# Heading', + '', + 'Your regular Markdown content follows...' ].join('\n') + , 'generates correct toc for file with Toml frontmatter' + ) + + t.end() +}) + +test('\ngiven a file that includes yaml frontmatter', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-yaml.md', 'utf8'); + var res = transform(content); + + t.same( + res.data + , [ '---', + 'title: Yaml Front Matter', + '---', + '', + '', + '', + '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*', + '', + '- [Heading](#heading)', + '', + '', + '', + '# Heading', + '', + 'Your regular Markdown content follows...' ].join('\n') + , 'generates correct toc for file with yaml frontmatter' + ) + + t.end() +}) + +test('\ngiven a file that includes invalid frontmatter', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-invalid.md', 'utf8'); + var res = transform(content); + + t.same( + res.data + , [ '', + '', + '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*', + '', + '- [Heading](#heading)', + '', + '', + '', + '+++', + 'title: Invalid Front Matter', + '|||', + '', + '# Heading', + '', + 'Your regular Markdown content follows...' ].join('\n') + , 'generates correct toc for file with invalid frontmatter' + ) + + t.end() +}) From 330c033b872503430576f993a9e6ba97ae9d8aec Mon Sep 17 00:00:00 2001 From: James Thompson Date: Fri, 17 Apr 2026 12:35:06 +1000 Subject: [PATCH 03/26] add new line at the end --- test/fixtures/readme-frontmatter-invalid.md | 2 +- test/fixtures/readme-frontmatter-json.md | 2 +- test/fixtures/readme-frontmatter-toml.md | 2 +- test/fixtures/readme-frontmatter-yaml.md | 2 +- test/transform-frontmatter.js | 12 ++++++++---- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/test/fixtures/readme-frontmatter-invalid.md b/test/fixtures/readme-frontmatter-invalid.md index b4cf2894..392a80b1 100644 --- a/test/fixtures/readme-frontmatter-invalid.md +++ b/test/fixtures/readme-frontmatter-invalid.md @@ -4,4 +4,4 @@ title: Invalid Front Matter # Heading -Your regular Markdown content follows... \ No newline at end of file +Your regular Markdown content follows... diff --git a/test/fixtures/readme-frontmatter-json.md b/test/fixtures/readme-frontmatter-json.md index a9b872a4..da256c3b 100644 --- a/test/fixtures/readme-frontmatter-json.md +++ b/test/fixtures/readme-frontmatter-json.md @@ -4,4 +4,4 @@ title: JSON Front Matter # Heading -Your regular Markdown content follows... \ No newline at end of file +Your regular Markdown content follows... diff --git a/test/fixtures/readme-frontmatter-toml.md b/test/fixtures/readme-frontmatter-toml.md index a3131f6c..7ffb9d15 100644 --- a/test/fixtures/readme-frontmatter-toml.md +++ b/test/fixtures/readme-frontmatter-toml.md @@ -4,4 +4,4 @@ title: TOML Front Matter # Heading -Your regular Markdown content follows... \ No newline at end of file +Your regular Markdown content follows... diff --git a/test/fixtures/readme-frontmatter-yaml.md b/test/fixtures/readme-frontmatter-yaml.md index 72f98b2c..d8e82d2c 100644 --- a/test/fixtures/readme-frontmatter-yaml.md +++ b/test/fixtures/readme-frontmatter-yaml.md @@ -4,4 +4,4 @@ title: Yaml Front Matter # Heading -Your regular Markdown content follows... \ No newline at end of file +Your regular Markdown content follows... diff --git a/test/transform-frontmatter.js b/test/transform-frontmatter.js index 00e0de00..f0b9fbc0 100644 --- a/test/transform-frontmatter.js +++ b/test/transform-frontmatter.js @@ -24,7 +24,8 @@ test('\ngiven a file that includes Json frontmatter', function (t) { '', '# Heading', '', - 'Your regular Markdown content follows...' ].join('\n') + 'Your regular Markdown content follows...', + '' ].join('\n') , 'generates correct toc for file with Json frontmatter' ) @@ -51,7 +52,8 @@ test('\ngiven a file that includes Toml frontmatter', function (t) { '', '# Heading', '', - 'Your regular Markdown content follows...' ].join('\n') + 'Your regular Markdown content follows...', + '' ].join('\n') , 'generates correct toc for file with Toml frontmatter' ) @@ -78,7 +80,8 @@ test('\ngiven a file that includes yaml frontmatter', function (t) { '', '# Heading', '', - 'Your regular Markdown content follows...' ].join('\n') + 'Your regular Markdown content follows...', + '' ].join('\n') , 'generates correct toc for file with yaml frontmatter' ) @@ -105,7 +108,8 @@ test('\ngiven a file that includes invalid frontmatter', function (t) { '', '# Heading', '', - 'Your regular Markdown content follows...' ].join('\n') + 'Your regular Markdown content follows...', + '' ].join('\n') , 'generates correct toc for file with invalid frontmatter' ) From 0feafb17296ba03cfe59e8d4baff0873a84cfcad Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 11:54:37 +1000 Subject: [PATCH 04/26] Add safeguards for not enough content. --- lib/transform.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 4af4b012..3d624b35 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -182,20 +182,27 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min var docNodes = md.parse(content); var tocs = getTocMarkers(docNodes, options?.toc?.pragma?.style, syntax); - if (!(tocs.positions?.length > 0) && updateOnly) { + if ((!(tocs.positions?.length > 0) && updateOnly) || docNodes.children.length < 1) { return { transformed: false }; } var startPos = 0; - if (docNodes.children[0].type == 'Yaml') { - startPos = docNodes.children[1].range[0]; + var frontMatter = false; + if (docNodes.children[0].type === 'Yaml') { + frontMatter = true; } - else if(docNodes.children[0].type == 'Paragraph' && docNodes.children[0].raw.startsWith('+++') && docNodes.children[0].raw.endsWith('+++')){ - startPos = docNodes.children[1].range[0]; + else if(docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith('+++') && docNodes.children[0].raw.trim().endsWith('+++')) { + frontMatter = true; + } + else if(docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.trim().endsWith(';;;')) { + frontMatter = true; } - else if(docNodes.children[0].type == 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.endsWith(';;;')){ + if (frontMatter && docNodes.children.length > 1) startPos = docNodes.children[1].range[0]; } + else if (frontMatter) { + return { transformed: false }; + } if (!(tocs.positions?.length > 0)) { var insertPos = startPos; From eec04c4953b3f38d1b3253632d45b72a5a4a606e Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 12:10:04 +1000 Subject: [PATCH 05/26] Update transform.js --- lib/transform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transform.js b/lib/transform.js index 3d624b35..152bc827 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -197,7 +197,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min else if(docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.trim().endsWith(';;;')) { frontMatter = true; } - if (frontMatter && docNodes.children.length > 1) + if (frontMatter && docNodes.children.length > 1) { startPos = docNodes.children[1].range[0]; } else if (frontMatter) { From 4eefc3982029d90f9a08afdd753d4bfe493901a7 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 12:38:17 +1000 Subject: [PATCH 06/26] Update transform.js --- lib/transform.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/transform.js b/lib/transform.js index 152bc827..9ad6f770 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -201,7 +201,10 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min startPos = docNodes.children[1].range[0]; } else if (frontMatter) { - return { transformed: false }; + startPos = docNodes.children[0].range[1] + 1; + } + if (docNodes.range[1] === docNodes.children[0].range[1]){ + content += '\n'; } if (!(tocs.positions?.length > 0)) { From a79acc1a623f1d1c36668e1759781f549362bab9 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 12:50:43 +1000 Subject: [PATCH 07/26] Update transform.js --- lib/transform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transform.js b/lib/transform.js index 9ad6f770..7d1d4a83 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -203,7 +203,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min else if (frontMatter) { startPos = docNodes.children[0].range[1] + 1; } - if (docNodes.range[1] === docNodes.children[0].range[1]){ + if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]){ content += '\n'; } From e43be0a6e9dc720cec1b3a48551d6fd6ccefb029 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 13:27:14 +1000 Subject: [PATCH 08/26] Add more tests --- test/transform-frontmatter.js | 79 +++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/test/transform-frontmatter.js b/test/transform-frontmatter.js index f0b9fbc0..d3f48a8f 100644 --- a/test/transform-frontmatter.js +++ b/test/transform-frontmatter.js @@ -12,7 +12,7 @@ test('\ngiven a file that includes Json frontmatter', function (t) { res.data , [ ';;;', 'title: JSON Front Matter', - ';;;', + ';;; ', '', '', '', @@ -40,7 +40,7 @@ test('\ngiven a file that includes Toml frontmatter', function (t) { res.data , [ '+++', 'title: TOML Front Matter', - '+++', + '+++ ', '', '', '', @@ -70,6 +70,8 @@ test('\ngiven a file that includes yaml frontmatter', function (t) { 'title: Yaml Front Matter', '---', '', + '# Title', + '', '', '', '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*', @@ -78,7 +80,7 @@ test('\ngiven a file that includes yaml frontmatter', function (t) { '', '', '', - '# Heading', + '## Heading', '', 'Your regular Markdown content follows...', '' ].join('\n') @@ -115,3 +117,74 @@ test('\ngiven a file that includes invalid frontmatter', function (t) { t.end() }) + +test('\ngiven a file that has only yaml frontmatter and empty line', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-blank.md', 'utf8'); + var res = transform(content.trim() + '\n'); + + t.same( + res.data + , [ '---', + 'title: Yaml Front Matter', + '---', + '', + '', + '', + '', + '' ].join('\n') + , 'generates correct toc for file with only yaml frontmatter' + ) + + t.end() +}) + +test('\ngiven a file that has only yaml frontmatter and no trailing whitespace', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-blank.md', 'utf8'); + var res = transform(content.trim()); + + t.same( + res.data + , [ '---', + 'title: Yaml Front Matter', + '---', + '', + '', + '', + '', + '' ].join('\n') + , 'generates correct toc for file with only yaml frontmatter' + ) + + t.end() +}) + +test('\ngiven a file that includes yaml frontmatter and process all', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-yaml.md', 'utf8'); + var res = transform(contentmode, undefined, undefined, undefined, undefined, undefined, undefined, true); + + t.same( + res.data + , [ '---', + 'title: Yaml Front Matter', + '---', + '', + '# Title', + '', + '', + '', + '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*', + '', + '- [Title](#title)', + ' - [Heading](#heading)', + '', + '', + '', + '## Heading', + '', + 'Your regular Markdown content follows...', + '' ].join('\n') + , 'generates correct toc for file with yaml frontmatter' + ) + + t.end() +}) From c6dff5ca73b36392648f1eb413ba573699e75ef2 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 13:42:51 +1000 Subject: [PATCH 09/26] update test fixtures --- test/fixtures/readme-frontmatter-blank.md | 3 +++ test/fixtures/readme-frontmatter-existing.yaml | 12 ++++++++++++ test/fixtures/readme-frontmatter-json.md | 2 +- test/fixtures/readme-frontmatter-toml.md | 2 +- test/transform-frontmatter.js | 6 ++---- 5 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/readme-frontmatter-blank.md create mode 100644 test/fixtures/readme-frontmatter-existing.yaml diff --git a/test/fixtures/readme-frontmatter-blank.md b/test/fixtures/readme-frontmatter-blank.md new file mode 100644 index 00000000..0cc3151d --- /dev/null +++ b/test/fixtures/readme-frontmatter-blank.md @@ -0,0 +1,3 @@ +--- +title: Yaml Front Matter +--- diff --git a/test/fixtures/readme-frontmatter-existing.yaml b/test/fixtures/readme-frontmatter-existing.yaml new file mode 100644 index 00000000..0f91c34b --- /dev/null +++ b/test/fixtures/readme-frontmatter-existing.yaml @@ -0,0 +1,12 @@ +--- +title: Yaml Front Matter +--- + +# Title + + + + +## Heading + +Your regular Markdown content follows... \ No newline at end of file diff --git a/test/fixtures/readme-frontmatter-json.md b/test/fixtures/readme-frontmatter-json.md index da256c3b..b9336505 100644 --- a/test/fixtures/readme-frontmatter-json.md +++ b/test/fixtures/readme-frontmatter-json.md @@ -1,6 +1,6 @@ ;;; title: JSON Front Matter -;;; +;;; # Heading diff --git a/test/fixtures/readme-frontmatter-toml.md b/test/fixtures/readme-frontmatter-toml.md index 7ffb9d15..305538d7 100644 --- a/test/fixtures/readme-frontmatter-toml.md +++ b/test/fixtures/readme-frontmatter-toml.md @@ -1,6 +1,6 @@ +++ title: TOML Front Matter -+++ ++++ # Heading diff --git a/test/transform-frontmatter.js b/test/transform-frontmatter.js index d3f48a8f..a675de54 100644 --- a/test/transform-frontmatter.js +++ b/test/transform-frontmatter.js @@ -70,8 +70,6 @@ test('\ngiven a file that includes yaml frontmatter', function (t) { 'title: Yaml Front Matter', '---', '', - '# Title', - '', '', '', '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*', @@ -80,7 +78,7 @@ test('\ngiven a file that includes yaml frontmatter', function (t) { '', '', '', - '## Heading', + '# Heading', '', 'Your regular Markdown content follows...', '' ].join('\n') @@ -159,7 +157,7 @@ test('\ngiven a file that has only yaml frontmatter and no trailing whitespace', }) test('\ngiven a file that includes yaml frontmatter and process all', function (t) { - var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-yaml.md', 'utf8'); + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-existing.md', 'utf8'); var res = transform(contentmode, undefined, undefined, undefined, undefined, undefined, undefined, true); t.same( From f4ae29a136fa97ebaa8349d3e78668fb4f5834d3 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 13:54:31 +1000 Subject: [PATCH 10/26] fix filename --- ...e-frontmatter-existing.yaml => readme-frontmatter-existing.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/fixtures/{readme-frontmatter-existing.yaml => readme-frontmatter-existing.md} (100%) diff --git a/test/fixtures/readme-frontmatter-existing.yaml b/test/fixtures/readme-frontmatter-existing.md similarity index 100% rename from test/fixtures/readme-frontmatter-existing.yaml rename to test/fixtures/readme-frontmatter-existing.md From 774e0eafa239f241fd4534410f8fddcbbf89cd5f Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 14:02:47 +1000 Subject: [PATCH 11/26] test fix --- lib/transform.js | 1 + test/transform-frontmatter.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/transform.js b/lib/transform.js index 7d1d4a83..512bc04d 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -205,6 +205,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min } if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]){ content += '\n'; + startPos++; } if (!(tocs.positions?.length > 0)) { diff --git a/test/transform-frontmatter.js b/test/transform-frontmatter.js index a675de54..34f117cb 100644 --- a/test/transform-frontmatter.js +++ b/test/transform-frontmatter.js @@ -158,7 +158,7 @@ test('\ngiven a file that has only yaml frontmatter and no trailing whitespace', test('\ngiven a file that includes yaml frontmatter and process all', function (t) { var content = require('fs').readFileSync(__dirname + '/fixtures/readme-frontmatter-existing.md', 'utf8'); - var res = transform(contentmode, undefined, undefined, undefined, undefined, undefined, undefined, true); + var res = transform(content, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true); t.same( res.data From 3549694b339c28739d7c0c506c7f222816287393 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 14:11:37 +1000 Subject: [PATCH 12/26] fix all test --- test/fixtures/readme-frontmatter-existing.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/fixtures/readme-frontmatter-existing.md b/test/fixtures/readme-frontmatter-existing.md index 0f91c34b..e29be9ea 100644 --- a/test/fixtures/readme-frontmatter-existing.md +++ b/test/fixtures/readme-frontmatter-existing.md @@ -4,9 +4,11 @@ title: Yaml Front Matter # Title - - + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + ## Heading -Your regular Markdown content follows... \ No newline at end of file +Your regular Markdown content follows... From 811c26a5cc572e498327c04897c06def5ba038c5 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 14:43:53 +1000 Subject: [PATCH 13/26] Update readme-frontmatter-existing.md --- test/fixtures/readme-frontmatter-existing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/fixtures/readme-frontmatter-existing.md b/test/fixtures/readme-frontmatter-existing.md index e29be9ea..2060c1a3 100644 --- a/test/fixtures/readme-frontmatter-existing.md +++ b/test/fixtures/readme-frontmatter-existing.md @@ -7,6 +7,8 @@ title: Yaml Front Matter **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Title](#title) ## Heading From 8e2da059c2fd6d32f86f6036eca19703e3e2a3db Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 14:49:33 +1000 Subject: [PATCH 14/26] Update transform.js --- lib/transform.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 512bc04d..3e6ff651 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -182,19 +182,19 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min var docNodes = md.parse(content); var tocs = getTocMarkers(docNodes, options?.toc?.pragma?.style, syntax); - if ((!(tocs.positions?.length > 0) && updateOnly) || docNodes.children.length < 1) { + if (!(tocs.positions?.length > 0) && updateOnly) { return { transformed: false }; } var startPos = 0; var frontMatter = false; - if (docNodes.children[0].type === 'Yaml') { + if (docNodes.children[0]?.type === 'Yaml') { frontMatter = true; } - else if(docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith('+++') && docNodes.children[0].raw.trim().endsWith('+++')) { + else if(docNodes.children[0]?.type === 'Paragraph' && docNodes.children[0].raw.startsWith('+++') && docNodes.children[0].raw.trim().endsWith('+++')) { frontMatter = true; } - else if(docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.trim().endsWith(';;;')) { + else if(docNodes.children[0]?.type === 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.trim().endsWith(';;;')) { frontMatter = true; } if (frontMatter && docNodes.children.length > 1) { From 2583873096fbe73a52bf9965a69f5da2c6b293fa Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 14:54:18 +1000 Subject: [PATCH 15/26] Update transform.js --- lib/transform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transform.js b/lib/transform.js index 3e6ff651..4e515b2f 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -205,7 +205,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min } if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]){ content += '\n'; - startPos++; + startPos = startPos + 2; } if (!(tocs.positions?.length > 0)) { From 655a610a245bfb708d07489b3adbaddbbc65ff15 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 15:10:34 +1000 Subject: [PATCH 16/26] Update transform.js --- lib/transform.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 4e515b2f..812fd7bd 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -200,12 +200,14 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min if (frontMatter && docNodes.children.length > 1) { startPos = docNodes.children[1].range[0]; } + // force it on to the next line with a line between else if (frontMatter) { - startPos = docNodes.children[0].range[1] + 1; + startPos = docNodes.children[0].range[1] + 2; + content += '\n'; } + // add additional line when doc has no trailing if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]){ content += '\n'; - startPos = startPos + 2; } if (!(tocs.positions?.length > 0)) { From ee25bb1b94532146086bfda1bad7ff3bfbf266af Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 17:46:29 +1000 Subject: [PATCH 17/26] Update transform.js --- lib/transform.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/transform.js b/lib/transform.js index 812fd7bd..4e03e08a 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -203,7 +203,6 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min // force it on to the next line with a line between else if (frontMatter) { startPos = docNodes.children[0].range[1] + 2; - content += '\n'; } // add additional line when doc has no trailing if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]){ From 343fb327b56e39de80fde350705aec496f794956 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sun, 19 Apr 2026 18:21:42 +1000 Subject: [PATCH 18/26] Update transform.js --- lib/transform.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 4e03e08a..466ffaa9 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -188,13 +188,14 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min var startPos = 0; var frontMatter = false; - if (docNodes.children[0]?.type === 'Yaml') { + var hasBody = docNodes.children.length > 0; + if (hasBody && docNodes.children[0].type === 'Yaml') { frontMatter = true; } - else if(docNodes.children[0]?.type === 'Paragraph' && docNodes.children[0].raw.startsWith('+++') && docNodes.children[0].raw.trim().endsWith('+++')) { + else if(hasBody && docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith('+++') && docNodes.children[0].raw.trim().endsWith('+++')) { frontMatter = true; } - else if(docNodes.children[0]?.type === 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.trim().endsWith(';;;')) { + else if(hasBody && docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.trim().endsWith(';;;')) { frontMatter = true; } if (frontMatter && docNodes.children.length > 1) { @@ -203,9 +204,10 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min // force it on to the next line with a line between else if (frontMatter) { startPos = docNodes.children[0].range[1] + 2; + hasBody = false; } // add additional line when doc has no trailing - if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]){ + if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]) { content += '\n'; } @@ -274,7 +276,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min if (transformed) { if (tocPosition.start.range[0] > 0) { data.push(content.slice(0, tocPosition.start.range[0] - 1)); } data.push(wrappedToc); - if (!tocPosition.existing) { data.push(''); } + if (!tocPosition.existing && hasBody) { data.push(''); } data.push(content.slice(tocPosition.end.range[1] + (tocPosition.existing ? 1 : 0))); } return { transformed : transformed, data : data.join('\n'), toc: toc, wrappedToc: wrappedToc }; From 47313e85f22b3d62f535dc162ff017ec769f9970 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Mon, 20 Apr 2026 01:51:27 +1000 Subject: [PATCH 19/26] Update transform.js --- lib/transform.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index e0eb2d1c..466ffaa9 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -211,8 +211,6 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min content += '\n'; } - var hasBody = docNodes.children.length > 0; - if (!(tocs.positions?.length > 0)) { var insertPos = startPos; tocs.positions = [{ From f54826e538536a2dffb659a2e7056a054d7ff870 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Tue, 21 Apr 2026 11:05:52 +1000 Subject: [PATCH 20/26] Update transform.js --- lib/transform.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 466ffaa9..a3dd3b4d 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -188,26 +188,27 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min var startPos = 0; var frontMatter = false; - var hasBody = docNodes.children.length > 0; - if (hasBody && docNodes.children[0].type === 'Yaml') { + var frontMatterDelimiters = ['+++', ';;;']; + var firstChild = docNodes.children.length > 0 ? docNodes.children[0] : undefined; + if (firstChild?.type === 'Yaml') { frontMatter = true; + } else if (firstChild?.type === 'Paragraph') { + frontMatter = frontMatterDelimiters.some(d => + firstChild.raw.startsWith(d) && firstChild.raw.trim().endsWith(d)); } - else if(hasBody && docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith('+++') && docNodes.children[0].raw.trim().endsWith('+++')) { - frontMatter = true; - } - else if(hasBody && docNodes.children[0].type === 'Paragraph' && docNodes.children[0].raw.startsWith(';;;') && docNodes.children[0].raw.trim().endsWith(';;;')) { - frontMatter = true; - } - if (frontMatter && docNodes.children.length > 1) { + + var hasBody = docNodes.children.length > frontMatter ? 1 : 0; + if (frontMatter && hasBody) { startPos = docNodes.children[1].range[0]; } - // force it on to the next line with a line between + // force it on to the next line after current content else if (frontMatter) { - startPos = docNodes.children[0].range[1] + 2; - hasBody = false; + startPos = docNode.range[1] + 1; } - // add additional line when doc has no trailing + + // adds in the empty line between end of content & new toc if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]) { + startPos = startPos + 1; content += '\n'; } From 6f2cff96b07742e524fd27637d53bd95455aae20 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Tue, 21 Apr 2026 11:14:28 +1000 Subject: [PATCH 21/26] Update transform.js --- lib/transform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transform.js b/lib/transform.js index a3dd3b4d..53468343 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -203,7 +203,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min } // force it on to the next line after current content else if (frontMatter) { - startPos = docNode.range[1] + 1; + startPos = docNodes.range[1] + 1; } // adds in the empty line between end of content & new toc From 408b826220a56dbf4434ce0109b3a613e19cc4be Mon Sep 17 00:00:00 2001 From: James Thompson Date: Tue, 21 Apr 2026 11:19:12 +1000 Subject: [PATCH 22/26] Update transform.js --- lib/transform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 53468343..7d6f8dab 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -197,7 +197,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min firstChild.raw.startsWith(d) && firstChild.raw.trim().endsWith(d)); } - var hasBody = docNodes.children.length > frontMatter ? 1 : 0; + var hasBody = docNodes.children.length > (frontMatter ? 1 : 0); if (frontMatter && hasBody) { startPos = docNodes.children[1].range[0]; } @@ -206,7 +206,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min startPos = docNodes.range[1] + 1; } - // adds in the empty line between end of content & new toc + // adds in the empty line between end of content & new toc if missing if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]) { startPos = startPos + 1; content += '\n'; From c9d6e77ba240fa420659dc3f9cc82065adcb21d5 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Wed, 22 Apr 2026 10:46:45 +1000 Subject: [PATCH 23/26] Use defined eol character --- lib/transform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index cf73e319..4484c5a3 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -209,8 +209,8 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min // adds in the empty line between end of content & new toc if missing if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]) { - startPos = startPos + 1; - content += '\n'; + startPos = startPos + eol.length; + content += eol; } if (!(tocs.positions?.length > 0)) { From 49b8b3f800888b8ebc51ae4f1353032323aa0d9d Mon Sep 17 00:00:00 2001 From: James Thompson Date: Sat, 25 Apr 2026 13:02:46 +1000 Subject: [PATCH 24/26] Merge branch 'master' into feat/#279_frontMatter --- README.md | 19 ++++ doctoc.js | 10 +- lib/transform.js | 16 +++- test/transform.js | 235 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4eb80a26..0f1f23db 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ by github or other sites via a command line flag. - [Max. heading level](#max-heading-level) - [Include all headings](#include-all-headings) - [Min. ToC items](#min-toc-items) + - [Min. Document Lines](#min-document-lines) - [Pad table of contents title](#pad-table-of-contents-title) - [Indentation Style](#indentation-style) - [TOC Pragma style](#toc-pragma-style) @@ -116,6 +117,24 @@ By default, - The min items is set to 1 +### Min. Document Lines + +Use the `--document-lines-min` option to specify the minimum lines required to be in a document for the document to have a table of contents; e.g., `doctoc --mintocitems 3 .`. + +By default, + +- The min Lines is set to 0 + +> [!NOTE] +> It is assumed that if you are using this option, you are using another tool such as +> MarkdownLint or Prettier to limit the length of lines to what is usually visible in a renderer. + +> [!TIP] +> +> If your document contains images, those images will not be counted any different to how plain text is, +> as doctoc is processing your document as a text processor. +> This also means repeated new lines will also be considered should your document contain them. + ### Pad table of contents title 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 .` diff --git a/doctoc.js b/doctoc.js index c24400ed..d28bd56b 100755 --- a/doctoc.js +++ b/doctoc.js @@ -97,7 +97,7 @@ var mode = modes["github"]; var argv = minimist(process.argv.slice(2), { boolean: [ 'h', 'help', 'T', 'notitle', 's', 'stdout', 'all' , 'u', 'update-only', 'd', 'dryrun'].concat(Object.keys(modes)), - string: [ 'title', 't', 'maxlevel', 'm', 'minlevel', 'entryprefix', 'syntax', 'mintocitems', 'toc-title-padding-before', 'toc-header-content', 'toc-footer-content', 'toc-pragma-style', 'toc-items-indentation-width', 'l', 'loglevel' ], + string: [ 'title', 't', 'maxlevel', 'm', 'minlevel', 'entryprefix', 'syntax', 'mintocitems', 'toc-title-padding-before', 'toc-header-content', 'toc-footer-content', 'toc-pragma-style', 'toc-items-indentation-width', 'document-lines-min', 'l', 'loglevel' ], unknown: function(a) { return (a[0] == '-' ? (console.error('Unknown option(s): ' + a), printUsageAndExit(true)) : true); } }); @@ -158,7 +158,15 @@ var indentWidth = argv['toc-items-indentation-width']; if (indentWidth !== undefined && isNaN(indentWidth)) { log.error('ToC indentation width: ' + indentWidth + ' is not a number'), printUsageAndExit(true); } else if (indentWidth === undefined) { indentWidth = (mode === 'bitbucket.org' || mode === 'gitlab.com') ? 4 : 2; } +var minLines = argv['document-lines-min'] || 0; +if (isNaN(minLines)) { log.error('Document min lines: ' + minLines + ' is not a number'), printUsageAndExit(true); } + var options = { + document: { + lines: { + min: Number(minLines) || 0, + } + }, toc: { pragma: { style: argv['toc-pragma-style'] || 'legacy', diff --git a/lib/transform.js b/lib/transform.js index 4484c5a3..a40aa8b2 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -187,6 +187,7 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min return { transformed: false }; } + var startLine = 0; var startPos = 0; var frontMatter = false; var frontMatterDelimiters = ['+++', ';;;']; @@ -197,22 +198,24 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min frontMatter = frontMatterDelimiters.some(d => firstChild.raw.startsWith(d) && firstChild.raw.trim().endsWith(d)); } - + var hasBody = docNodes.children.length > (frontMatter ? 1 : 0); if (frontMatter && hasBody) { + startLine = docNodes.children[1].loc.start.line - 1; startPos = docNodes.children[1].range[0]; } // force it on to the next line after current content else if (frontMatter) { + startLine = docNodes.children[0].loc.end.line; startPos = docNodes.range[1] + 1; } - + // adds in the empty line between end of content & new toc if missing if (frontMatter && docNodes.range[1] === docNodes.children[0].range[1]) { startPos = startPos + eol.length; content += eol; } - + if (!(tocs.positions?.length > 0)) { var insertPos = startPos; tocs.positions = [{ @@ -234,8 +237,13 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min .concat(getHtmlHeaders(docNodes, tocableStart, maxHeaderLevelHtml, minHeaderLevel)); var toc = ''; + var wrappedToc; + var minLines = options?.document?.lines?.min + startLine || startLine; + if (tocPosition.existing) { + minLines = minLines + tocPosition.end.loc.end.line - tocPosition.start.loc.start.line + 2; + } - if (headers.length >= minTocItems) { + 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); diff --git a/test/transform.js b/test/transform.js index 26c1106f..117e83b4 100644 --- a/test/transform.js +++ b/test/transform.js @@ -418,6 +418,241 @@ check( ].join('') ) +check( + [ '# Test 1' + , '' + , '# Test 2' + ].join('\n') + , [ '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*\n\n' + , '- [Test 1](#test-1)\n', + , '- [Test 2](#test-2)\n\n\n' + ].join('') + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , { + document: { + lines: { + min: 3 + } + } + } +) + +check( + [ '---' + , 'title: a' + , '---' + , '' + , '# Test 1' + , '' + , '# Test 2' + ].join('\n') + , [ '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*\n\n' + , '- [Test 1](#test-1)\n', + , '- [Test 2](#test-2)\n\n\n' + ].join('') + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , { + document: { + lines: { + min: 3 + } + } + } +) + +check( + [ '# Test 1' + , '' + , '# Test 2' + ].join('\n') + , [].join('') + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , { + document: { + lines: { + min: 4 + } + } + } +) + +check( + [ '---' + , 'title: a' + , '---' + , '' + , '# Test 1' + , '' + , '# Test 2' + ].join('\n') + , [].join('') + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , { + document: { + lines: { + min: 4 + } + } + } +) + +check( + [ ';;;' + , 'title: a' + , ';;;' + , '' + , '# Test 1' + , '' + , '# Test 2' + ].join('\n') + , [].join('') + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , { + document: { + lines: { + min: 4 + } + } + } +) + +check( + [ '+++' + , 'title: a' + , '+++' + , '' + , '# Test 1' + , '' + , '# Test 2' + ].join('\n') + , [].join('') + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , { + document: { + lines: { + min: 4 + } + } + } +) + +test('\ndmin document lines', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-with-notitle.md', 'utf8'); + var headers = transform(content + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , { + document: { + lines: { + min: 9 + } + } + }); + + t.same( + headers.toc + , '' + , 'generates a correct toc when readme includes duplicate headings with different symbols' + ) + t.end() +}); + +test('\ndmin document lines', function (t) { + var content = require('fs').readFileSync(__dirname + '/fixtures/readme-with-notitle.md', 'utf8'); + var headers = transform(content + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , undefined + , { + document: { + lines: { + min: 8 + } + } + }); + + t.same( + headers.toc + , [ '', + "- [Installation](#installation)", + "- [API](#api)", + "- [License](#license)", + "" + ].join('\n') + , 'generates a correct toc when readme includes duplicate headings with different symbols' + ) + t.end() +}); + test('transforming when old toc exists', function (t) { var md = [ '# Header above' From ce64fbbaea33234184c37edbe5dc9c2f2fe6cc68 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Mon, 27 Apr 2026 14:05:59 +1000 Subject: [PATCH 25/26] Improve test to cater for TOC not always first --- lib/transform.js | 6 ++++++ test/transform.js | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/transform.js b/lib/transform.js index a40aa8b2..49be5317 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -215,6 +215,12 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min startPos = startPos + eol.length; content += eol; } + + // if there is no empty line between front matter & content add one + if (frontMatter && hasBody && docNodes.children[0].loc.end.line + 1 == docNodes.children[1].loc.start.line) { + content = content.substring(0, startPos) + eol + content.substring(startPos); + startPos = startPos + eol.length; + } if (!(tocs.positions?.length > 0)) { var insertPos = startPos; diff --git a/test/transform.js b/test/transform.js index 117e83b4..784f0bfa 100644 --- a/test/transform.js +++ b/test/transform.js @@ -12,10 +12,23 @@ function check(md, anchors, mode, maxHeaderLevel, minHeaderLevel, minTocItems, t // build the expected content eol = eol || '\n'; var pragma = contentGenerator.pragmaMarkers(syntax || 'md'); + var body = ''; + var doc = ''; + var tag = md.substring(0, 3); + const tags = ['---', ';;;', '+++']; + if (tags.includes(tag) && md.indexOf(tag, 3) > 0) { + var pos = md.indexOf(tag, 3) + 3 + eol.length; // find the closing tag and skip that line + body = md.substring(pos); + if (body.startsWith(eol)) { body = body.substring(eol.length); } + doc = md.substring(0, pos) + eol; + } + else { + body = md; + } var contents = anchors.trimEnd(); if (contents != '') { contents += eol; } var toc = pragma.start + eol + anchors.trimEnd() + (anchors ? eol + eol : '') + pragma.end; - var doc = res.wrappedToc + eol + eol + md; + doc = doc + res.wrappedToc + eol + eol + body; t.ok(res.transformed, 'transforms it'); t.same(res.toc, contents, 'generates correct toc contents'); @@ -968,6 +981,29 @@ check( , 'mdx' ) +check( + [ '---' + , '' + , '# Real Heading' + , 'body' + ].join('\n') +, [ '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*\n\n' + , '- [Real Heading](#real-heading)\n\n\n' + ].join('') +) + +check( + [ '---' + , 'title: Yaml' + , '---' + , '# H' + , 'body' + ].join('\n') +, [ '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*\n\n' + , '- [H](#h)\n\n\n' + ].join('') +) + test('should use comments if syntax=md', function (t) { var res = transform( [ '# My Module' From e9e8b1755ebe0ca114531592a8dc93c4716ef25c Mon Sep 17 00:00:00 2001 From: James Thompson Date: Mon, 27 Apr 2026 14:09:25 +1000 Subject: [PATCH 26/26] Bump markdown parser --- lib/transform.js | 9 +-------- package-lock.json | 16 ++++++++-------- package.json | 2 +- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 49be5317..79ffaa73 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -189,15 +189,8 @@ exports = module.exports = function transform(content, mode, maxHeaderLevel, min var startLine = 0; var startPos = 0; - var frontMatter = false; - var frontMatterDelimiters = ['+++', ';;;']; var firstChild = docNodes.children.length > 0 ? docNodes.children[0] : undefined; - if (firstChild?.type === 'Yaml') { - frontMatter = true; - } else if (firstChild?.type === 'Paragraph') { - frontMatter = frontMatterDelimiters.some(d => - firstChild.raw.startsWith(d) && firstChild.raw.trim().endsWith(d)); - } + var frontMatter = firstChild?.type === 'Json' || firstChild?.type === 'Toml' || firstChild?.type === 'Yaml'; var hasBody = docNodes.children.length > (frontMatter ? 1 : 0); if (frontMatter && hasBody) { diff --git a/package-lock.json b/package-lock.json index e357d7b2..f9511715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-development", "license": "MIT", "dependencies": { - "@textlint/markdown-to-ast": "^15.5.2", + "@textlint/markdown-to-ast": "^15.6.0", "anchor-markdown-header": "^0.8.3", "htmlparser2": "^7.2.0", "loglevel": "^1.9.2", @@ -925,18 +925,18 @@ } }, "node_modules/@textlint/ast-node-types": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.5.2.tgz", - "integrity": "sha512-fCaOxoup5LIyBEo7R1oYWE7V4bSX0KQeHh66twon9e9usaLE3ijgF8QjYsR6joCssdeCHVd0wHm7ppsEyTr6vg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.6.0.tgz", + "integrity": "sha512-CxZHFbYAU7J0A4izz31wV2ZZfySR6aVj2OSR6/3tppZm7VV6hM7nA7sutsLwIiBL/v4lsB1RM79l4Dc/VrH4qw==", "license": "MIT" }, "node_modules/@textlint/markdown-to-ast": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@textlint/markdown-to-ast/-/markdown-to-ast-15.5.2.tgz", - "integrity": "sha512-eLEeIb7jcyWiG1jahr3pl3z8dEYEgLPdz7lBG3AP8aB4m8Sv0SdL0mCD0tfR5hNp8FrOEXldqh1g2PMuiXlN3w==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/markdown-to-ast/-/markdown-to-ast-15.6.0.tgz", + "integrity": "sha512-JEuExLk5wvI3ZD0uuWiEmFKwXEpGsmESwf7SeN1xda1xFvZyeEmi1Nk53hmkHp5l1mTMC+UUxFt50EiZ30KDUQ==", "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "15.5.2", + "@textlint/ast-node-types": "15.6.0", "debug": "^4.4.3", "mdast-util-gfm-autolink-literal": "^0.1.3", "neotraverse": "^0.6.18", diff --git a/package.json b/package.json index c487b310..bbff5f86 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "main": "doctoc.js", "bin": "doctoc.js", "dependencies": { - "@textlint/markdown-to-ast": "^15.5.2", + "@textlint/markdown-to-ast": "^15.6.0", "anchor-markdown-header": "^0.8.3", "htmlparser2": "^7.2.0", "loglevel": "^1.9.2",