Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 62 additions & 62 deletions lib/markdown-parser.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,9 +13,9 @@ export class MarkdownTreeParser {
constructor(options = {}) {
this.options = {
// Default remark-stringify options
bullet: "*",
emphasis: "*",
strong: "*",
bullet: '*',
emphasis: '*',
strong: '*',
...options,
};

Expand Down Expand Up @@ -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) {
Expand All @@ -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(
Expand All @@ -114,7 +114,7 @@ export class MarkdownTreeParser {
const copiedNodes = JSON.parse(JSON.stringify(sectionNodes));

return {
type: "root",
type: 'root',
children: copiedNodes,
};
}
Expand All @@ -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);
Expand All @@ -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]),
Expand All @@ -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
) {
Expand All @@ -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]),
Expand All @@ -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]),
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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),
Expand All @@ -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)
Expand All @@ -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) {
Expand All @@ -314,7 +314,7 @@ export class MarkdownTreeParser {
const copiedNodes = JSON.parse(JSON.stringify(sectionNodes));

return {
type: "root",
type: 'root',
children: copiedNodes,
};
}
Expand All @@ -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+/)
Expand All @@ -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`;
});
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down