diff --git a/lib/markdown-parser.js b/lib/markdown-parser.js index 4bceced..57258cc 100644 --- a/lib/markdown-parser.js +++ b/lib/markdown-parser.js @@ -1,9 +1,9 @@ -import { unified } from "unified"; -import remarkParse from "remark-parse"; -import remarkStringify from "remark-stringify"; -import { visit } from "unist-util-visit"; -import { selectAll, select } from "unist-util-select"; -import { find } from "unist-util-find"; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkStringify from 'remark-stringify'; +import { visit } from 'unist-util-visit'; +import { selectAll, select } from 'unist-util-select'; +import { find } from 'unist-util-find'; /** * A powerful markdown parser that treats markdown as a manipulable tree structure @@ -13,9 +13,9 @@ export class MarkdownTreeParser { constructor(options = {}) { this.options = { // Default remark-stringify options - bullet: "*", - emphasis: "*", - strong: "*", + bullet: '*', + emphasis: '*', + strong: '*', ...options, }; @@ -63,23 +63,26 @@ export class MarkdownTreeParser { let exactMatchIndex = -1; // Find the target heading - prefer exact matches over partial matches - visit(tree, "heading", (node, index, _parent) => { - const nodeText = this.getHeadingText(node); - const lowerNodeText = nodeText.toLowerCase(); - const lowerSearchText = headingText.toLowerCase(); - - if (level === null || node.depth === level) { - // Check for exact match first - if (lowerNodeText === lowerSearchText) { - exactMatch = node; - exactMatchIndex = index; - } else if (lowerNodeText.includes(lowerSearchText) && !foundHeading) { - // Only use partial match if no exact match found yet and no other partial match - foundHeading = node; - startIndex = index; + for (let i = 0; i < tree.children.length; i++) { + const node = tree.children[i]; + if (node.type === 'heading') { + const nodeText = this.getHeadingText(node); + const lowerNodeText = nodeText.toLowerCase(); + const lowerSearchText = headingText.toLowerCase(); + + if (level === null || node.depth === level) { + // Check for exact match first + if (lowerNodeText === lowerSearchText) { + exactMatch = node; + exactMatchIndex = i; + } else if (lowerNodeText.includes(lowerSearchText) && !foundHeading) { + // Only use partial match if no exact match found yet and no other partial match + foundHeading = node; + startIndex = i; + } } } - }); + } // Prefer exact match over partial match if (exactMatch) { @@ -93,16 +96,13 @@ export class MarkdownTreeParser { // Find where this section ends (next heading of same or higher level) const targetDepth = foundHeading.depth; - visit(tree, (node, index) => { - if ( - index > startIndex && - node.type === "heading" && - node.depth <= targetDepth - ) { - endIndex = index; - return "skip"; + for (let i = startIndex + 1; i < tree.children.length; i++) { + const node = tree.children[i]; + if (node.type === 'heading' && node.depth <= targetDepth) { + endIndex = i; + break; } - }); + } // Create a new tree with just this section const sectionNodes = tree.children.slice( @@ -114,7 +114,7 @@ export class MarkdownTreeParser { const copiedNodes = JSON.parse(JSON.stringify(sectionNodes)); return { - type: "root", + type: 'root', children: copiedNodes, }; } @@ -132,7 +132,7 @@ export class MarkdownTreeParser { for (let i = 0; i < tree.children.length; i++) { const node = tree.children[i]; - if (node.type === "heading" && node.depth === level) { + if (node.type === 'heading' && node.depth === level) { // If we have a previous section, save it if (startIndex !== -1) { const sectionNodes = tree.children.slice(startIndex, i); @@ -141,7 +141,7 @@ export class MarkdownTreeParser { sections.push({ heading: JSON.parse(JSON.stringify(tree.children[startIndex])), tree: { - type: "root", + type: 'root', children: copiedNodes, }, headingText: this.getHeadingText(tree.children[startIndex]), @@ -151,7 +151,7 @@ export class MarkdownTreeParser { // Start new section startIndex = i; } else if ( - node.type === "heading" && + node.type === 'heading' && node.depth <= level && startIndex !== -1 ) { @@ -162,7 +162,7 @@ export class MarkdownTreeParser { sections.push({ heading: JSON.parse(JSON.stringify(tree.children[startIndex])), tree: { - type: "root", + type: 'root', children: copiedNodes, }, headingText: this.getHeadingText(tree.children[startIndex]), @@ -179,7 +179,7 @@ export class MarkdownTreeParser { sections.push({ heading: JSON.parse(JSON.stringify(tree.children[startIndex])), tree: { - type: "root", + type: 'root', children: copiedNodes, }, headingText: this.getHeadingText(tree.children[startIndex]), @@ -216,11 +216,11 @@ export class MarkdownTreeParser { * @returns {Object|null} First matching node or null */ findNode(tree, condition) { - if (typeof condition === "string") { + if (typeof condition === 'string') { return find(tree, condition); - } else if (typeof condition === "function") { + } else if (typeof condition === 'function') { return find(tree, condition); - } else if (typeof condition === "object") { + } else if (typeof condition === 'object') { return find(tree, condition); } return null; @@ -232,8 +232,8 @@ export class MarkdownTreeParser { * @returns {string} Plain text content of the heading */ getHeadingText(headingNode) { - let text = ""; - visit(headingNode, "text", (node) => { + let text = ''; + visit(headingNode, 'text', (node) => { text += node.value; }); return text; @@ -257,7 +257,7 @@ export class MarkdownTreeParser { */ getHeadingsList(tree) { const headings = []; - visit(tree, "heading", (node) => { + visit(tree, 'heading', (node) => { headings.push({ level: node.depth, text: this.getHeadingText(node), @@ -280,7 +280,7 @@ export class MarkdownTreeParser { const sectionNodes = []; visit(tree, (node, _index) => { - if (node.type === "heading") { + if (node.type === 'heading') { if ( !collecting && this.getHeadingText(node) @@ -293,7 +293,7 @@ export class MarkdownTreeParser { } else if (collecting) { // Stop collecting if we hit a heading of same or higher level if (node.depth <= foundHeading.depth) { - return "skip"; + return 'skip'; } // Stop if we've exceeded max depth if (maxDepth && node.depth > foundHeading.depth + maxDepth) { @@ -314,7 +314,7 @@ export class MarkdownTreeParser { const copiedNodes = JSON.parse(JSON.stringify(sectionNodes)); return { - type: "root", + type: 'root', children: copiedNodes, }; } @@ -337,27 +337,27 @@ export class MarkdownTreeParser { visit(tree, (node) => { switch (node.type) { - case "heading": + case 'heading': stats.headings.total++; stats.headings.byLevel[node.depth] = (stats.headings.byLevel[node.depth] || 0) + 1; break; - case "paragraph": + case 'paragraph': stats.paragraphs++; break; - case "code": + case 'code': stats.codeBlocks++; break; - case "list": + case 'list': stats.lists++; break; - case "link": + case 'link': stats.links++; break; - case "image": + case 'image': stats.images++; break; - case "text": + case 'text': stats.wordCount += node.value .trim() .split(/\s+/) @@ -380,19 +380,19 @@ export class MarkdownTreeParser { const filteredHeadings = headings.filter((h) => h.level <= maxLevel); if (filteredHeadings.length === 0) { - return ""; + return ''; } - let toc = "## Table of Contents\n\n"; + let toc = '## Table of Contents\n\n'; filteredHeadings.forEach((heading) => { - const indent = " ".repeat(heading.level - 1); + const indent = ' '.repeat(heading.level - 1); const link = heading.text .toLowerCase() - .replace(/[^a-z0-9\s-]/g, "") - .replace(/\s+/g, "-") - .replace(/-+/g, "-") - .replace(/^-|-$/g, ""); + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); toc += `${indent}- [${heading.text}](#${link})\n`; }); diff --git a/package-lock.json b/package-lock.json index 78b374e..2f85afd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@kayvan/markdown-tree-parser", - "version": "1.4.0", + "version": "1.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@kayvan/markdown-tree-parser", - "version": "1.4.0", + "version": "1.4.2", "license": "MIT", "dependencies": { "remark-parse": "^11.0.0", @@ -20,9 +20,9 @@ "md-tree": "bin/md-tree.js" }, "devDependencies": { - "@eslint/js": "^9.27.0", - "@types/node": "^22.15.27", - "eslint": "^9.27.0", + "@eslint/js": "^9.28.0", + "@types/node": "^22.15.29", + "eslint": "^9.28.0", "prettier": "^3.5.3" }, "engines": { diff --git a/package.json b/package.json index f02a856..8644862 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kayvan/markdown-tree-parser", - "version": "1.4.1", + "version": "1.4.2", "description": "A powerful JavaScript library and CLI tool for parsing and manipulating markdown files as tree structures using the remark/unified ecosystem", "type": "module", "main": "index.js",