Skip to content

Commit 7ec274b

Browse files
committed
feat: preserve heading hierarchy in markdown explode/assemble operations
## CHANGES - Keep exploded sections at level 2 headings - Generate rich TOC with all subsections - Parse AST to build nested navigation - Create anchor links for subsections - Remove heading level adjustments in assembly - Add helper to create URL-friendly anchors - Preserve original document structure throughout
1 parent 4865c25 commit 7ec274b

File tree

1 file changed

+100
-24
lines changed

1 file changed

+100
-24
lines changed

bin/md-tree.js

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -535,8 +535,8 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
535535
for (const section of sections) {
536536
const headingText = section.headingText;
537537

538-
// Convert the heading to level 1 and preserve all original content
539-
const sectionLines = [`# ${headingText}`, ...section.lines];
538+
// Keep the heading at level 2 for proper document structure
539+
const sectionLines = [`## ${headingText}`, ...section.lines];
540540
const sectionContent = sectionLines.join('\n');
541541

542542
// Generate filename without numbered prefix
@@ -552,8 +552,12 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
552552
console.log(`✅ ${headingText}${filename}`);
553553
}
554554

555-
// Generate index.md with original title and TOC pointing to files
556-
const indexContent = await this.generateIndexContentTextBased(content, sectionFiles);
555+
// Parse content with AST to generate rich TOC with all subsections
556+
const tree = await this.parser.parse(content);
557+
const indexContent = await this.generateIndexContentWithSubsections(
558+
tree,
559+
sectionFiles
560+
);
557561
const indexPath = path.join(outputDir, 'index.md');
558562
await this.writeFile(indexPath, indexContent);
559563
console.log(`✅ Table of Contents → index.md`);
@@ -564,10 +568,90 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
564568
}
565569

566570
async generateIndexContent(tree, sectionFiles) {
567-
// Use the text-based approach for consistency
568-
// Convert the tree back to text to get the original content
569-
const originalContent = await this.parser.stringify(tree);
570-
return await this.generateIndexContentTextBased(originalContent, sectionFiles);
571+
// Use the enhanced AST-based approach to include all subsections
572+
return await this.generateIndexContentWithSubsections(tree, sectionFiles);
573+
}
574+
575+
// Enhanced index generation with all subsections using AST
576+
async generateIndexContentWithSubsections(tree, sectionFiles) {
577+
const headings = this.parser.getHeadingsList(tree);
578+
const mainTitle = headings.find((h) => h.level === 1);
579+
580+
if (!mainTitle) {
581+
return await this.generateIndexContentTextBased(
582+
await this.parser.stringify(tree),
583+
sectionFiles
584+
);
585+
}
586+
587+
// Create a map of section names to filenames for quick lookup
588+
const sectionMap = new Map();
589+
sectionFiles.forEach((file) => {
590+
sectionMap.set(file.headingText.toLowerCase(), file.filename);
591+
});
592+
593+
// Start with title and TOC heading
594+
let toc = `# ${mainTitle.text}\n\n## Table of Contents\n\n`;
595+
596+
// Add the main title link
597+
toc += `- [${mainTitle.text}](#table-of-contents)\n`;
598+
599+
// Process all headings to create nested TOC
600+
let currentLevel2Filename = null;
601+
602+
for (const heading of headings) {
603+
// Skip the main title (level 1)
604+
if (heading.level === 1) {
605+
continue;
606+
}
607+
608+
if (heading.level === 2) {
609+
// This is a main section
610+
currentLevel2Filename = sectionMap.get(heading.text.toLowerCase());
611+
612+
if (currentLevel2Filename) {
613+
toc += ` - [${heading.text}](./${currentLevel2Filename})\n`;
614+
} else {
615+
toc += ` - [${heading.text}](#${this.createAnchor(heading.text)})\n`;
616+
}
617+
} else if (heading.level > 2 && currentLevel2Filename) {
618+
// This is a subsection within a level 2 section
619+
const indent = ' '.repeat(heading.level - 2);
620+
const anchor = this.createAnchor(heading.text);
621+
toc += `${indent}- [${heading.text}](./${currentLevel2Filename}#${anchor})\n`;
622+
}
623+
}
624+
625+
return toc;
626+
}
627+
628+
// Helper to create URL-friendly anchors
629+
createAnchor(text) {
630+
return text
631+
.toLowerCase()
632+
.replace(/[^a-z0-9\s-]/g, '')
633+
.replace(/\s+/g, '-')
634+
.replace(/-+/g, '-')
635+
.replace(/^-|-$/g, '');
636+
}
637+
638+
// Method to decrement ALL heading levels in text (shift all headings down one level)
639+
decrementAllHeadingLevelsInText(content) {
640+
const lines = content.split('\n');
641+
642+
const adjustedLines = lines.map((line) => {
643+
// Check if line is a heading (starts with #)
644+
const headingMatch = line.match(/^(#{1,5})(\s+.*)$/);
645+
if (headingMatch) {
646+
const [, hashes, rest] = headingMatch;
647+
// Add one more # to decrease the level (level 1 becomes level 2, etc.)
648+
// Only do this for levels 1-5 to avoid going beyond level 6
649+
return '#' + hashes + rest;
650+
}
651+
return line;
652+
});
653+
654+
return adjustedLines.join('\n');
571655
}
572656

573657
// Generate index content preserving original spacing
@@ -714,14 +798,10 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
714798
try {
715799
const sectionContent = await this.readFile(filePath);
716800

717-
// Work directly with text to preserve formatting
718-
const adjustedContent =
719-
this.incrementHeadingLevelsInText(sectionContent);
720-
721-
// Add the section content:
801+
// Add the section content directly - exploded files already have correct heading levels
722802
// - After main title: blank line then content (original has blank line after title)
723803
// - Between sections: direct concatenation (original has no spacing between sections)
724-
assembledContent += '\n' + adjustedContent;
804+
assembledContent += '\n' + sectionContent;
725805
} catch {
726806
console.error(
727807
`⚠️ Warning: Could not read ${sectionFile.filename}, skipping...`
@@ -732,25 +812,21 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
732812
// Write the assembled document
733813
await this.writeFile(outputFile, assembledContent);
734814
console.log(`\n✨ Document assembled to ${outputFile}`);
735-
} // New method to increment heading levels directly in text without AST roundtrip
815+
} // Method to increment ALL heading levels directly in text (shift all headings up one level)
736816
incrementHeadingLevelsInText(content) {
737817
const lines = content.split('\n');
738-
let isFirstHeading = true;
739818

740819
const adjustedLines = lines.map((line) => {
741820
// Check if line is a heading (starts with #)
742821
const headingMatch = line.match(/^(#{1,6})(\s+.*)$/);
743822
if (headingMatch) {
744823
const [, hashes, rest] = headingMatch;
745-
746-
// Only increment the first heading (the main section heading)
747-
// This converts the level 1 section heading back to level 2
748-
if (isFirstHeading && hashes === '#') {
749-
isFirstHeading = false;
750-
return '##' + rest;
824+
// Remove one # to increase the level (level 2 becomes level 1, etc.)
825+
// Only do this for levels 2-6 to avoid going below level 1
826+
if (hashes.length > 1) {
827+
return hashes.slice(1) + rest;
751828
}
752-
753-
// All other headings remain at their current level
829+
// If it's already level 1, keep it at level 1 (can't go higher)
754830
return line;
755831
}
756832
return line;

0 commit comments

Comments
 (0)