@@ -69,28 +69,30 @@ class MarkdownCLI {
6969Usage: md-tree <command> <file> [options]
7070
7171Commands:
72- list <file> List all headings in the file
73- extract <file> <heading> Extract a specific section by heading text
74- extract-all <file> [level] Extract all sections at level (default: 2)
75- explode-doc <file> <output-dir> Extract all level 2 sections and create index
76- tree <file> Show the document structure as a tree
77- search <file> <selector> Search using CSS-like selectors
78- stats <file> Show document statistics
79- toc <file> Generate table of contents
80- version Show version information
81- help Show this help message
72+ list <file> List all headings in the file
73+ extract <file> <heading> Extract a specific section by heading text
74+ extract-all <file> [level] Extract all sections at level (default: 2)
75+ explode <file> <output-dir> Extract all level 2 sections and create index
76+ assemble <dir> <output-file> Reassemble exploded document from directory
77+ tree <file> Show the document structure as a tree
78+ search <file> <selector> Search using CSS-like selectors
79+ stats <file> Show document statistics
80+ toc <file> Generate table of contents
81+ version Show version information
82+ help Show this help message
8283
8384Options:
84- --output, -o <dir> Output directory for extracted files
85- --level, -l <number> Heading level to work with
86- --format, -f <json|text> Output format (default: text)
87- --max-level <number> Maximum heading level for TOC (default: 3)
85+ --output, -o <dir> Output directory for extracted files
86+ --level, -l <number> Heading level to work with
87+ --format, -f <json|text> Output format (default: text)
88+ --max-level <number> Maximum heading level for TOC (default: 3)
8889
8990Examples:
9091 md-tree list README.md
9192 md-tree extract README.md "Installation"
9293 md-tree extract-all README.md 2 --output ./sections
93- md-tree explode-doc README.md ./exploded
94+ md-tree explode README.md ./exploded
95+ md-tree assemble ./exploded reassembled.md
9496 md-tree tree README.md
9597 md-tree search README.md "heading[depth=2]"
9698 md-tree stats README.md
@@ -389,17 +391,28 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
389391 break ;
390392 }
391393
392- case 'explode-doc ' : {
394+ case 'explode' : {
393395 if ( args . length < 3 ) {
394396 console . error (
395- '❌ Usage: md-tree explode-doc <file> <output-directory>'
397+ '❌ Usage: md-tree explode <file> <output-directory>'
396398 ) ;
397399 process . exit ( 1 ) ;
398400 }
399401 await this . explodeDocument ( args [ 1 ] , args [ 2 ] ) ;
400402 break ;
401403 }
402404
405+ case 'assemble' : {
406+ if ( args . length < 3 ) {
407+ console . error (
408+ '❌ Usage: md-tree assemble <directory> <output-file>'
409+ ) ;
410+ process . exit ( 1 ) ;
411+ }
412+ await this . assembleDocument ( args [ 1 ] , args [ 2 ] ) ;
413+ break ;
414+ }
415+
403416 case 'tree' :
404417 if ( args . length < 2 ) {
405418 console . error ( '❌ Usage: md-tree tree <file>' ) ;
@@ -601,8 +614,129 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
601614
602615 return clonedTree ;
603616 }
617+
618+ // Helper method to increment all heading levels in a tree by 1
619+ incrementHeadingLevels ( tree ) {
620+ if ( ! tree || ! tree . children ) return tree ;
621+
622+ // Create a deep copy to avoid modifying the original tree
623+ const clonedTree = JSON . parse ( JSON . stringify ( tree ) ) ;
624+
625+ const incrementNode = ( node ) => {
626+ if ( node . type === 'heading' && node . depth < 6 ) {
627+ node . depth = node . depth + 1 ;
628+ }
629+
630+ if ( node . children ) {
631+ node . children . forEach ( incrementNode ) ;
632+ }
633+ } ;
634+
635+ if ( clonedTree . children ) {
636+ clonedTree . children . forEach ( incrementNode ) ;
637+ }
638+
639+ return clonedTree ;
640+ }
641+
642+ async assembleDocument ( inputDir , outputFile ) {
643+ const indexPath = path . join ( inputDir , 'index.md' ) ;
644+
645+ // Check if index.md exists
646+ try {
647+ await fs . access ( indexPath ) ;
648+ } catch ( _error ) {
649+ console . error ( `❌ index.md not found in ${ inputDir } ` ) ;
650+ process . exit ( 1 ) ;
651+ }
652+
653+ const indexContent = await this . readFile ( indexPath ) ;
654+ const indexTree = await this . parser . parse ( indexContent ) ;
655+
656+ // Extract the main title and get the list of section files from TOC
657+ const headings = this . parser . getHeadingsList ( indexTree ) ;
658+ const mainTitle = headings . find ( ( h ) => h . level === 1 ) ;
659+
660+ if ( ! mainTitle ) {
661+ console . error ( '❌ No main title found in index.md' ) ;
662+ process . exit ( 1 ) ;
663+ }
664+
665+ console . log ( `\n📚 Assembling document: ${ mainTitle . text } ` ) ;
666+
667+ // Parse the TOC to extract section file references
668+ const sectionFiles = await this . extractSectionFilesFromTOC ( indexTree ) ;
669+
670+ if ( sectionFiles . length === 0 ) {
671+ console . error ( '❌ No section files found in TOC' ) ;
672+ process . exit ( 1 ) ;
673+ }
674+
675+ console . log ( `📖 Found ${ sectionFiles . length } sections to assemble` ) ;
676+
677+ // Start building the reassembled document
678+ let assembledContent = `# ${ mainTitle . text } \n\n` ;
679+
680+ // Process each section file
681+ for ( const sectionFile of sectionFiles ) {
682+ console . log ( `✅ Processing ${ sectionFile . filename } ...` ) ;
683+
684+ const filePath = path . join ( inputDir , sectionFile . filename ) ;
685+ try {
686+ const sectionContent = await this . readFile ( filePath ) ;
687+ const sectionTree = await this . parser . parse ( sectionContent ) ;
688+
689+ // Increment heading levels by 1 to restore original structure
690+ const adjustedTree = this . incrementHeadingLevels ( sectionTree ) ;
691+ const sectionMarkdown = await this . parser . stringify ( adjustedTree ) ;
692+
693+ // Remove the leading heading since it will be a level 2 now
694+ assembledContent += sectionMarkdown + '\n\n' ;
695+ } catch ( _error ) {
696+ console . error (
697+ `⚠️ Warning: Could not read ${ sectionFile . filename } , skipping...`
698+ ) ;
699+ }
700+ }
701+
702+ // Write the assembled document
703+ await this . writeFile ( outputFile , assembledContent . trim ( ) ) ;
704+ console . log ( `\n✨ Document assembled to ${ outputFile } ` ) ;
705+ }
706+
707+ async extractSectionFilesFromTOC ( indexTree ) {
708+ // Convert the tree back to markdown to parse the TOC links
709+ const indexMarkdown = await this . parser . stringify ( indexTree ) ;
710+ const lines = indexMarkdown . split ( '\n' ) ;
711+
712+ const sectionFiles = [ ] ;
713+ const processedFiles = new Set ( ) ;
714+
715+ for ( const line of lines ) {
716+ // Look for TOC lines that reference files (not just anchors)
717+ const match = line . match ( / \[ ( [ ^ \] ] + ) \] \( \. \/ ( [ ^ # ) ] + ) (?: # [ ^ ) ] * ) ? \) / ) ;
718+ if ( match ) {
719+ const [ , linkText , filename ] = match ;
720+
721+ // Only include level 2 sections (main sections, not sub-sections)
722+ // Level 2 items have exactly 2 spaces before the dash (are children of main heading)
723+ // Level 3+ items have 4+ spaces (are nested deeper)
724+ if ( line . match ( / ^ { 2 } [ - * ] \[ / ) && ! processedFiles . has ( filename ) ) {
725+ sectionFiles . push ( {
726+ filename,
727+ title : linkText ,
728+ } ) ;
729+ processedFiles . add ( filename ) ;
730+ }
731+ }
732+ }
733+
734+ return sectionFiles ;
735+ }
604736}
605737
606- // Run CLI
738+ // Export the class for testing
739+ export { MarkdownCLI } ;
740+
607741const cli = new MarkdownCLI ( ) ;
608742cli . run ( ) ;
0 commit comments