Skip to content

Commit ee99b1f

Browse files
authored
Merge pull request #2 from ksylvan/0531-bmad-extract-section-fix
Fix section extraction bug in extractSection method
2 parents c3952c3 + 23e61f1 commit ee99b1f

File tree

3 files changed

+68
-68
lines changed

3 files changed

+68
-68
lines changed

lib/markdown-parser.js

Lines changed: 62 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { unified } from "unified";
2-
import remarkParse from "remark-parse";
3-
import remarkStringify from "remark-stringify";
4-
import { visit } from "unist-util-visit";
5-
import { selectAll, select } from "unist-util-select";
6-
import { find } from "unist-util-find";
1+
import { unified } from 'unified';
2+
import remarkParse from 'remark-parse';
3+
import remarkStringify from 'remark-stringify';
4+
import { visit } from 'unist-util-visit';
5+
import { selectAll, select } from 'unist-util-select';
6+
import { find } from 'unist-util-find';
77

88
/**
99
* A powerful markdown parser that treats markdown as a manipulable tree structure
@@ -13,9 +13,9 @@ export class MarkdownTreeParser {
1313
constructor(options = {}) {
1414
this.options = {
1515
// Default remark-stringify options
16-
bullet: "*",
17-
emphasis: "*",
18-
strong: "*",
16+
bullet: '*',
17+
emphasis: '*',
18+
strong: '*',
1919
...options,
2020
};
2121

@@ -63,23 +63,26 @@ export class MarkdownTreeParser {
6363
let exactMatchIndex = -1;
6464

6565
// Find the target heading - prefer exact matches over partial matches
66-
visit(tree, "heading", (node, index, _parent) => {
67-
const nodeText = this.getHeadingText(node);
68-
const lowerNodeText = nodeText.toLowerCase();
69-
const lowerSearchText = headingText.toLowerCase();
70-
71-
if (level === null || node.depth === level) {
72-
// Check for exact match first
73-
if (lowerNodeText === lowerSearchText) {
74-
exactMatch = node;
75-
exactMatchIndex = index;
76-
} else if (lowerNodeText.includes(lowerSearchText) && !foundHeading) {
77-
// Only use partial match if no exact match found yet and no other partial match
78-
foundHeading = node;
79-
startIndex = index;
66+
for (let i = 0; i < tree.children.length; i++) {
67+
const node = tree.children[i];
68+
if (node.type === 'heading') {
69+
const nodeText = this.getHeadingText(node);
70+
const lowerNodeText = nodeText.toLowerCase();
71+
const lowerSearchText = headingText.toLowerCase();
72+
73+
if (level === null || node.depth === level) {
74+
// Check for exact match first
75+
if (lowerNodeText === lowerSearchText) {
76+
exactMatch = node;
77+
exactMatchIndex = i;
78+
} else if (lowerNodeText.includes(lowerSearchText) && !foundHeading) {
79+
// Only use partial match if no exact match found yet and no other partial match
80+
foundHeading = node;
81+
startIndex = i;
82+
}
8083
}
8184
}
82-
});
85+
}
8386

8487
// Prefer exact match over partial match
8588
if (exactMatch) {
@@ -93,16 +96,13 @@ export class MarkdownTreeParser {
9396

9497
// Find where this section ends (next heading of same or higher level)
9598
const targetDepth = foundHeading.depth;
96-
visit(tree, (node, index) => {
97-
if (
98-
index > startIndex &&
99-
node.type === "heading" &&
100-
node.depth <= targetDepth
101-
) {
102-
endIndex = index;
103-
return "skip";
99+
for (let i = startIndex + 1; i < tree.children.length; i++) {
100+
const node = tree.children[i];
101+
if (node.type === 'heading' && node.depth <= targetDepth) {
102+
endIndex = i;
103+
break;
104104
}
105-
});
105+
}
106106

107107
// Create a new tree with just this section
108108
const sectionNodes = tree.children.slice(
@@ -114,7 +114,7 @@ export class MarkdownTreeParser {
114114
const copiedNodes = JSON.parse(JSON.stringify(sectionNodes));
115115

116116
return {
117-
type: "root",
117+
type: 'root',
118118
children: copiedNodes,
119119
};
120120
}
@@ -132,7 +132,7 @@ export class MarkdownTreeParser {
132132
for (let i = 0; i < tree.children.length; i++) {
133133
const node = tree.children[i];
134134

135-
if (node.type === "heading" && node.depth === level) {
135+
if (node.type === 'heading' && node.depth === level) {
136136
// If we have a previous section, save it
137137
if (startIndex !== -1) {
138138
const sectionNodes = tree.children.slice(startIndex, i);
@@ -141,7 +141,7 @@ export class MarkdownTreeParser {
141141
sections.push({
142142
heading: JSON.parse(JSON.stringify(tree.children[startIndex])),
143143
tree: {
144-
type: "root",
144+
type: 'root',
145145
children: copiedNodes,
146146
},
147147
headingText: this.getHeadingText(tree.children[startIndex]),
@@ -151,7 +151,7 @@ export class MarkdownTreeParser {
151151
// Start new section
152152
startIndex = i;
153153
} else if (
154-
node.type === "heading" &&
154+
node.type === 'heading' &&
155155
node.depth <= level &&
156156
startIndex !== -1
157157
) {
@@ -162,7 +162,7 @@ export class MarkdownTreeParser {
162162
sections.push({
163163
heading: JSON.parse(JSON.stringify(tree.children[startIndex])),
164164
tree: {
165-
type: "root",
165+
type: 'root',
166166
children: copiedNodes,
167167
},
168168
headingText: this.getHeadingText(tree.children[startIndex]),
@@ -179,7 +179,7 @@ export class MarkdownTreeParser {
179179
sections.push({
180180
heading: JSON.parse(JSON.stringify(tree.children[startIndex])),
181181
tree: {
182-
type: "root",
182+
type: 'root',
183183
children: copiedNodes,
184184
},
185185
headingText: this.getHeadingText(tree.children[startIndex]),
@@ -216,11 +216,11 @@ export class MarkdownTreeParser {
216216
* @returns {Object|null} First matching node or null
217217
*/
218218
findNode(tree, condition) {
219-
if (typeof condition === "string") {
219+
if (typeof condition === 'string') {
220220
return find(tree, condition);
221-
} else if (typeof condition === "function") {
221+
} else if (typeof condition === 'function') {
222222
return find(tree, condition);
223-
} else if (typeof condition === "object") {
223+
} else if (typeof condition === 'object') {
224224
return find(tree, condition);
225225
}
226226
return null;
@@ -232,8 +232,8 @@ export class MarkdownTreeParser {
232232
* @returns {string} Plain text content of the heading
233233
*/
234234
getHeadingText(headingNode) {
235-
let text = "";
236-
visit(headingNode, "text", (node) => {
235+
let text = '';
236+
visit(headingNode, 'text', (node) => {
237237
text += node.value;
238238
});
239239
return text;
@@ -257,7 +257,7 @@ export class MarkdownTreeParser {
257257
*/
258258
getHeadingsList(tree) {
259259
const headings = [];
260-
visit(tree, "heading", (node) => {
260+
visit(tree, 'heading', (node) => {
261261
headings.push({
262262
level: node.depth,
263263
text: this.getHeadingText(node),
@@ -280,7 +280,7 @@ export class MarkdownTreeParser {
280280
const sectionNodes = [];
281281

282282
visit(tree, (node, _index) => {
283-
if (node.type === "heading") {
283+
if (node.type === 'heading') {
284284
if (
285285
!collecting &&
286286
this.getHeadingText(node)
@@ -293,7 +293,7 @@ export class MarkdownTreeParser {
293293
} else if (collecting) {
294294
// Stop collecting if we hit a heading of same or higher level
295295
if (node.depth <= foundHeading.depth) {
296-
return "skip";
296+
return 'skip';
297297
}
298298
// Stop if we've exceeded max depth
299299
if (maxDepth && node.depth > foundHeading.depth + maxDepth) {
@@ -314,7 +314,7 @@ export class MarkdownTreeParser {
314314
const copiedNodes = JSON.parse(JSON.stringify(sectionNodes));
315315

316316
return {
317-
type: "root",
317+
type: 'root',
318318
children: copiedNodes,
319319
};
320320
}
@@ -337,27 +337,27 @@ export class MarkdownTreeParser {
337337

338338
visit(tree, (node) => {
339339
switch (node.type) {
340-
case "heading":
340+
case 'heading':
341341
stats.headings.total++;
342342
stats.headings.byLevel[node.depth] =
343343
(stats.headings.byLevel[node.depth] || 0) + 1;
344344
break;
345-
case "paragraph":
345+
case 'paragraph':
346346
stats.paragraphs++;
347347
break;
348-
case "code":
348+
case 'code':
349349
stats.codeBlocks++;
350350
break;
351-
case "list":
351+
case 'list':
352352
stats.lists++;
353353
break;
354-
case "link":
354+
case 'link':
355355
stats.links++;
356356
break;
357-
case "image":
357+
case 'image':
358358
stats.images++;
359359
break;
360-
case "text":
360+
case 'text':
361361
stats.wordCount += node.value
362362
.trim()
363363
.split(/\s+/)
@@ -380,19 +380,19 @@ export class MarkdownTreeParser {
380380
const filteredHeadings = headings.filter((h) => h.level <= maxLevel);
381381

382382
if (filteredHeadings.length === 0) {
383-
return "";
383+
return '';
384384
}
385385

386-
let toc = "## Table of Contents\n\n";
386+
let toc = '## Table of Contents\n\n';
387387

388388
filteredHeadings.forEach((heading) => {
389-
const indent = " ".repeat(heading.level - 1);
389+
const indent = ' '.repeat(heading.level - 1);
390390
const link = heading.text
391391
.toLowerCase()
392-
.replace(/[^a-z0-9\s-]/g, "")
393-
.replace(/\s+/g, "-")
394-
.replace(/-+/g, "-")
395-
.replace(/^-|-$/g, "");
392+
.replace(/[^a-z0-9\s-]/g, '')
393+
.replace(/\s+/g, '-')
394+
.replace(/-+/g, '-')
395+
.replace(/^-|-$/g, '');
396396

397397
toc += `${indent}- [${heading.text}](#${link})\n`;
398398
});

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kayvan/markdown-tree-parser",
3-
"version": "1.4.1",
3+
"version": "1.4.2",
44
"description": "A powerful JavaScript library and CLI tool for parsing and manipulating markdown files as tree structures using the remark/unified ecosystem",
55
"type": "module",
66
"main": "index.js",

0 commit comments

Comments
 (0)