@mgks/docmd 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,27 @@
1
1
  // Source file from the docmd project — https://github.com/mgks/docmd
2
2
 
3
3
  const fs = require('fs-extra');
4
- const MarkdownIt = require('markdown-it');
5
- const matter = require('gray-matter');
6
- const hljs = require('highlight.js');
7
- const attrs = require('markdown-it-attrs');
8
4
  const path = require('path');
9
- const markdown_it_footnote = require('markdown-it-footnote');
10
- const markdown_it_task_lists = require('markdown-it-task-lists');
11
- const markdown_it_abbr = require('markdown-it-abbr');
12
- const markdown_it_deflist = require('markdown-it-deflist');
5
+ const matter = require('gray-matter');
6
+ const { createMarkdownItInstance } = require('./markdown/setup');
7
+
8
+ function decodeHtmlEntities(html) {
9
+ return html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
10
+ }
11
+
12
+ function extractHeadingsFromHtml(htmlContent) {
13
+ const headings = [];
14
+ const headingRegex = /<h([1-6])[^>]*?id="([^"]*)"[^>]*?>([\s\S]*?)<\/h\1>/g;
15
+ let match;
16
+ while ((match = headingRegex.exec(htmlContent)) !== null) {
17
+ const level = parseInt(match[1], 10);
18
+ const id = match[2];
19
+ const text = decodeHtmlEntities(match[3].replace(/<\/?[^>]+(>|$)/g, ''));
20
+ headings.push({ id, level, text });
21
+ }
22
+ return headings;
23
+ }
13
24
 
14
- /**
15
- * Formats an absolute path to be relative to the current working directory for cleaner logging.
16
- */
17
25
  function formatPathForDisplay(absolutePath) {
18
26
  const CWD = process.cwd();
19
27
  const relativePath = path.relative(CWD, absolutePath);
@@ -23,783 +31,47 @@ function formatPathForDisplay(absolutePath) {
23
31
  return relativePath;
24
32
  }
25
33
 
26
- function createMarkdownItInstance(config) {
27
- const mdOptions = {
28
- html: true,
29
- linkify: true,
30
- typographer: true,
31
- breaks: true,
32
- };
33
-
34
- // Conditionally enable highlighting
35
- if (config.theme?.codeHighlight !== false) {
36
- mdOptions.highlight = function (str, lang) {
37
- // Handle mermaid diagrams
38
- if (lang === 'mermaid') {
39
- return `<pre class="mermaid">${new MarkdownIt().utils.escapeHtml(str)}</pre>`;
40
- }
41
-
42
- if (lang && hljs.getLanguage(lang)) {
43
- try {
44
- return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
45
- } catch (e) { console.error(`Highlighting error for lang ${lang}:`, e); }
46
- }
47
- return `<pre class="hljs"><code>${new MarkdownIt().utils.escapeHtml(str)}</code></pre>`;
48
- };
49
- } else {
50
- // Even if highlighting is disabled, we need to handle mermaid
51
- mdOptions.highlight = function (str, lang) {
52
- if (lang === 'mermaid') {
53
- return `<pre class="mermaid">${new MarkdownIt().utils.escapeHtml(str)}</pre>`;
54
- }
55
- return `<pre><code>${new MarkdownIt().utils.escapeHtml(str)}</code></pre>`;
56
- };
57
- }
58
-
59
- const md = new MarkdownIt(mdOptions);
60
-
61
- // --- Attach all plugins and rules to this instance ---
62
- md.use(attrs, { leftDelimiter: '{', rightDelimiter: '}' });
63
- md.use(markdown_it_footnote);
64
- md.use(markdown_it_task_lists);
65
- md.use(markdown_it_abbr);
66
- md.use(markdown_it_deflist);
67
- md.use(headingIdPlugin);
68
-
69
- // Register renderers for all containers
70
- Object.keys(containers).forEach(containerName => {
71
- const container = containers[containerName];
72
- md.renderer.rules[`container_${containerName}_open`] = container.render;
73
- md.renderer.rules[`container_${containerName}_close`] = container.render;
74
- });
75
-
76
- // Register the enhanced rules
77
- md.block.ruler.before('fence', 'steps_container', stepsContainerRule, {
78
- alt: ['paragraph', 'reference', 'blockquote', 'list']
79
- });
80
- md.block.ruler.before('fence', 'enhanced_tabs', enhancedTabsRule, {
81
- alt: ['paragraph', 'reference', 'blockquote', 'list']
82
- });
83
- md.block.ruler.before('paragraph', 'advanced_container', advancedContainerRule, {
84
- alt: ['paragraph', 'reference', 'blockquote', 'list']
85
- });
86
-
87
- // Register all custom renderers
88
- md.renderer.rules.ordered_list_open = customOrderedListOpenRenderer;
89
- md.renderer.rules.list_item_open = customListItemOpenRenderer;
90
- md.renderer.rules.image = customImageRenderer;
91
-
92
- // Wrap tables in a container for horizontal scrolling
93
- md.renderer.rules.table_open = function(tokens, idx, options, env, self) {
94
- return '<div class="table-wrapper">' + self.renderToken(tokens, idx, options);
95
- };
96
- md.renderer.rules.table_close = function(tokens, idx, options, env, self) {
97
- return self.renderToken(tokens, idx, options) + '</div>';
98
- };
99
-
100
- // Register tabs renderers
101
- md.renderer.rules.tabs_open = tabsOpenRenderer;
102
- md.renderer.rules.tabs_nav_open = tabsNavOpenRenderer;
103
- md.renderer.rules.tabs_nav_close = tabsNavCloseRenderer;
104
- md.renderer.rules.tabs_nav_item = tabsNavItemRenderer;
105
- md.renderer.rules.tabs_content_open = tabsContentOpenRenderer;
106
- md.renderer.rules.tabs_content_close = tabsContentCloseRenderer;
107
- md.renderer.rules.tab_pane_open = tabPaneOpenRenderer;
108
- md.renderer.rules.tab_pane_close = tabPaneCloseRenderer;
109
- md.renderer.rules.tabs_close = tabsCloseRenderer;
110
-
111
- // Register heading ID plugin
112
- md.use(headingIdPlugin);
113
-
114
- // Register standalone closing rule
115
- md.block.ruler.before('paragraph', 'standalone_closing', standaloneClosingRule, {
116
- alt: ['paragraph', 'reference', 'blockquote', 'list']
117
- });
118
-
119
- return md;
120
- }
121
-
122
- // ===================================================================
123
- // --- ADVANCED NESTED CONTAINER SYSTEM ---
124
- // ===================================================================
125
-
126
- // Container definitions
127
- // To add a new container type:
128
- // 1. Add it to this containers object
129
- // 2. Define the render function for opening (nesting === 1) and closing (nesting === -1)
130
- // 3. The system will automatically register it and support nesting
131
- const containers = {
132
- card: {
133
- name: 'card',
134
- render: (tokens, idx) => {
135
- if (tokens[idx].nesting === 1) {
136
- const title = tokens[idx].info ? tokens[idx].info.trim() : '';
137
- return `<div class="docmd-container card">${title ? `<div class="card-title">${title}</div>` : ''}<div class="card-content">`;
138
- }
139
- return '</div></div>';
140
- }
141
- },
142
- callout: {
143
- name: 'callout',
144
- render: (tokens, idx) => {
145
- if (tokens[idx].nesting === 1) {
146
- const [type, ...titleParts] = tokens[idx].info.split(' ');
147
- const title = titleParts.join(' ');
148
- return `<div class="docmd-container callout callout-${type}">${title ? `<div class="callout-title">${title}</div>` : ''}<div class="callout-content">`;
149
- }
150
- return '</div></div>';
151
- }
152
- },
153
- button: {
154
- name: 'button',
155
- selfClosing: true, // Mark as self-closing
156
- render: (tokens, idx) => {
157
- if (tokens[idx].nesting === 1) {
158
- const parts = tokens[idx].info.split(' ');
159
- const text = parts[0];
160
- const url = parts[1];
161
- const color = parts[2];
162
- const colorStyle = color && color.startsWith('color:') ? ` style="background-color: ${color.split(':')[1]}"` : '';
163
-
164
- // Check if URL starts with 'external:' for new tab behavior
165
- let finalUrl = url;
166
- let targetAttr = '';
167
- if (url && url.startsWith('external:')) {
168
- finalUrl = url.substring(9); // Remove 'external:' prefix
169
- targetAttr = ' target="_blank" rel="noopener noreferrer"';
170
- }
171
-
172
- return `<a href="${finalUrl}" class="docmd-button"${colorStyle}${targetAttr}>${text.replace(/_/g, ' ')}</a>`;
173
- }
174
- return '';
175
- }
176
- },
177
- steps: {
178
- name: 'steps',
179
- render: (tokens, idx) => {
180
- if (tokens[idx].nesting === 1) {
181
- // Add a unique class for steps containers to enable CSS-based numbering reset
182
- // The steps-numbering class will style only direct ol > li children as numbered steps
183
- return '<div class="docmd-container steps steps-reset steps-numbering">';
184
- }
185
- return '</div>';
186
- }
187
- }
188
- // Future containers can be added here:
189
- // timeline: {
190
- // name: 'timeline',
191
- // render: (tokens, idx) => {
192
- // if (tokens[idx].nesting === 1) {
193
- // return '<div class="docmd-container timeline">';
194
- // }
195
- // return '</div>';
196
- // }
197
- // },
198
- // changelog: {
199
- // name: 'changelog',
200
- // render: (tokens, idx) => {
201
- // if (tokens[idx].nesting === 1) {
202
- // return '<div class="docmd-container changelog">';
203
- // }
204
- // return '</div>';
205
- // }
206
- // }
207
- };
208
-
209
- // Advanced container rule with proper nesting support
210
- function advancedContainerRule(state, startLine, endLine, silent) {
211
- const start = state.bMarks[startLine] + state.tShift[startLine];
212
- const max = state.eMarks[startLine];
213
- const lineContent = state.src.slice(start, max).trim();
214
-
215
- // Check if this is a container opening
216
- const containerMatch = lineContent.match(/^:::\s*(\w+)(?:\s+(.+))?$/);
217
- if (!containerMatch) return false;
218
-
219
- const [, containerName, params] = containerMatch;
220
- const container = containers[containerName];
221
-
222
- if (!container) return false;
223
-
224
- if (silent) return true;
225
-
226
- // Handle self-closing containers (like buttons)
227
- if (container.selfClosing) {
228
- const openToken = state.push(`container_${containerName}_open`, 'div', 1);
229
- openToken.info = params || '';
230
- const closeToken = state.push(`container_${containerName}_close`, 'div', -1);
231
- state.line = startLine + 1;
232
- return true;
233
- }
234
-
235
- // Find the closing tag with proper nesting handling
236
- let nextLine = startLine;
237
- let found = false;
238
- let depth = 1;
239
-
240
- while (nextLine < endLine) {
241
- nextLine++;
242
- const nextStart = state.bMarks[nextLine] + state.tShift[nextLine];
243
- const nextMax = state.eMarks[nextLine];
244
- const nextContent = state.src.slice(nextStart, nextMax).trim();
245
-
246
- // Check for opening tags (any container)
247
- if (nextContent.startsWith(':::')) {
248
- const containerMatch = nextContent.match(/^:::\s*(\w+)/);
249
- if (containerMatch && containerMatch[1] !== containerName) {
250
- // Only increment depth for non-self-closing containers
251
- const innerContainer = containers[containerMatch[1]];
252
- if (innerContainer && innerContainer.render && !innerContainer.selfClosing) {
253
- depth++;
254
- }
255
- continue;
256
- }
257
- }
258
-
259
- // Check for closing tags
260
- if (nextContent === ':::') {
261
- depth--;
262
- if (depth === 0) {
263
- found = true;
264
- break;
265
- }
266
- }
267
- }
268
-
269
- if (!found) return false;
270
-
271
- // Create tokens
272
- const openToken = state.push(`container_${containerName}_open`, 'div', 1);
273
- openToken.info = params || '';
274
-
275
- // Process content recursively
276
- const oldParentType = state.parentType;
277
- const oldLineMax = state.lineMax;
278
-
279
- state.parentType = 'container';
280
- state.lineMax = nextLine;
281
-
282
- // Process the content inside the container
283
- state.md.block.tokenize(state, startLine + 1, nextLine);
284
-
285
- const closeToken = state.push(`container_${containerName}_close`, 'div', -1);
286
-
287
- state.parentType = oldParentType;
288
- state.lineMax = oldLineMax;
289
- state.line = nextLine + 1;
290
-
291
- return true;
292
- }
293
-
294
- // --- Simple Steps Container Rule ---
295
- function stepsContainerRule(state, startLine, endLine, silent) {
296
- const start = state.bMarks[startLine] + state.tShift[startLine];
297
- const max = state.eMarks[startLine];
298
- const lineContent = state.src.slice(start, max).trim();
299
- if (lineContent !== '::: steps') return false;
300
- if (silent) return true;
301
-
302
- // Find the closing ':::' for the steps container
303
- let nextLine = startLine;
304
- let found = false;
305
- let depth = 1;
34
+ async function processMarkdownFile(filePath, md, config) {
35
+ const rawContent = await fs.readFile(filePath, 'utf8');
36
+ let frontmatter, markdownContent;
306
37
 
307
- while (nextLine < endLine) {
308
- nextLine++;
309
- const nextStart = state.bMarks[nextLine] + state.tShift[nextLine];
310
- const nextMax = state.eMarks[nextLine];
311
- const nextContent = state.src.slice(nextStart, nextMax).trim();
312
-
313
- // Skip tab markers as they don't affect container depth
314
- if (nextContent.startsWith('== tab')) {
315
- continue;
316
- }
317
-
318
- // Check for opening tags (any container)
319
- if (nextContent.startsWith(':::')) {
320
- const containerMatch = nextContent.match(/^:::\s*(\w+)/);
321
- if (containerMatch) {
322
- const containerName = containerMatch[1];
323
- // Only count non-self-closing containers for depth
324
- const innerContainer = containers[containerName];
325
- if (innerContainer && !innerContainer.selfClosing) {
326
- depth++;
327
- }
328
- continue;
329
- }
330
- }
331
-
332
- // Check for closing tags
333
- if (nextContent === ':::') {
334
- depth--;
335
- if (depth === 0) {
336
- found = true;
337
- break;
338
- }
38
+ try {
39
+ ({ data: frontmatter, content: markdownContent } = matter(rawContent));
40
+ } catch (e) {
41
+ console.error(`❌ Error parsing frontmatter in ${formatPathForDisplay(filePath)}:`);
42
+ console.error(` ${e.message}`);
43
+ return null;
339
44
  }
340
- }
341
-
342
- if (!found) return false;
343
-
344
- // Create tokens for steps container
345
- const openToken = state.push('container_steps_open', 'div', 1);
346
- openToken.info = '';
347
-
348
- // Process content normally but disable automatic list processing
349
- const oldParentType = state.parentType;
350
- const oldLineMax = state.lineMax;
351
45
 
352
- state.parentType = 'container';
353
- state.lineMax = nextLine;
354
-
355
- // Process the content inside the container
356
- state.md.block.tokenize(state, startLine + 1, nextLine);
357
-
358
- const closeToken = state.push('container_steps_close', 'div', -1);
359
-
360
- state.parentType = oldParentType;
361
- state.lineMax = oldLineMax;
362
- state.line = nextLine + 1;
363
-
364
- return true;
365
- }
366
-
367
- // --- Enhanced tabs rule with nested content support ---
368
- function enhancedTabsRule(state, startLine, endLine, silent) {
369
- const start = state.bMarks[startLine] + state.tShift[startLine];
370
- const max = state.eMarks[startLine];
371
- const lineContent = state.src.slice(start, max).trim();
372
-
373
- if (lineContent !== '::: tabs') return false;
374
- if (silent) return true;
375
-
376
- // Find the closing tag with proper nesting handling
377
- let nextLine = startLine;
378
- let found = false;
379
- let depth = 1;
380
- while (nextLine < endLine) {
381
- nextLine++;
382
- const nextStart = state.bMarks[nextLine] + state.tShift[nextLine];
383
- const nextMax = state.eMarks[nextLine];
384
- const nextContent = state.src.slice(nextStart, nextMax).trim();
385
-
386
- // Check for opening tags (any container)
387
- if (nextContent.startsWith(':::')) {
388
- const containerMatch = nextContent.match(/^:::\s*(\w+)/);
389
- if (containerMatch && containerMatch[1] !== 'tabs') {
390
- // Don't increment depth for steps - they have their own depth counting
391
- if (containerMatch[1] === 'steps') {
392
- continue;
393
- }
394
- // Only increment depth for non-self-closing containers
395
- const innerContainer = containers[containerMatch[1]];
396
- if (innerContainer && !innerContainer.selfClosing) {
397
- depth++;
398
- }
399
- continue;
400
- }
401
- }
402
-
403
- // Check for closing tags
404
- if (nextContent === ':::') {
405
- depth--;
406
- if (depth === 0) {
407
- found = true;
408
- break;
409
- }
46
+ if (!frontmatter.title && config.autoTitleFromH1 !== false) {
47
+ const h1Match = markdownContent.match(/^#\s+(.*)/m);
48
+ if (h1Match) frontmatter.title = h1Match[1].trim();
410
49
  }
411
- }
412
- if (!found) return false;
413
-
414
- // Get the raw content by manually extracting lines
415
- let content = '';
416
- for (let i = startLine + 1; i < nextLine; i++) {
417
- const lineStart = state.bMarks[i] + state.tShift[i];
418
- const lineEnd = state.eMarks[i];
419
- content += state.src.slice(lineStart, lineEnd) + '\n';
420
- }
421
-
422
- // Parse tabs manually
423
- const lines = content.split('\n');
424
- const tabs = [];
425
- let currentTab = null;
426
- let currentContent = [];
427
50
 
428
- for (let i = 0; i < lines.length; i++) {
429
- const line = lines[i].trim();
430
- const tabMatch = line.match(/^==\s*tab\s+(?:"([^"]+)"|(\S+))$/);
431
-
432
- if (tabMatch) {
433
- // Save previous tab if exists
434
- if (currentTab) {
435
- currentTab.content = currentContent.join('\n').trim();
436
- tabs.push(currentTab);
437
- }
438
- // Start new tab
439
- const title = tabMatch[1] || tabMatch[2];
440
- currentTab = { title: title, content: '' };
441
- currentContent = [];
442
- } else if (currentTab) {
443
- // Add line to current tab content (only if not empty and not a tab marker)
444
- if (lines[i].trim() && !lines[i].trim().startsWith('==')) {
445
- currentContent.push(lines[i]);
446
- }
51
+ let htmlContent, headings;
52
+ if (frontmatter.noStyle === true) {
53
+ htmlContent = markdownContent;
54
+ headings = [];
55
+ } else {
56
+ htmlContent = md.render(markdownContent);
57
+ headings = extractHeadingsFromHtml(htmlContent);
447
58
  }
448
- }
449
59
 
450
- // Save the last tab
451
- if (currentTab) {
452
- currentTab.content = currentContent.join('\n').trim();
453
- tabs.push(currentTab);
454
- }
455
-
456
- // Create tabs structure
457
- const openToken = state.push('tabs_open', 'div', 1);
458
- openToken.attrs = [['class', 'docmd-tabs']];
459
-
460
- // Create navigation
461
- const navToken = state.push('tabs_nav_open', 'div', 1);
462
- navToken.attrs = [['class', 'docmd-tabs-nav']];
463
- tabs.forEach((tab, index) => {
464
- const navItemToken = state.push('tabs_nav_item', 'div', 0);
465
- navItemToken.attrs = [['class', `docmd-tabs-nav-item ${index === 0 ? 'active' : ''}`]];
466
- navItemToken.content = tab.title;
467
- });
468
- state.push('tabs_nav_close', 'div', -1);
469
-
470
- // Create content
471
- const contentToken = state.push('tabs_content_open', 'div', 1);
472
- contentToken.attrs = [['class', 'docmd-tabs-content']];
473
- tabs.forEach((tab, index) => {
474
- const paneToken = state.push('tab_pane_open', 'div', 1);
475
- paneToken.attrs = [['class', `docmd-tab-pane ${index === 0 ? 'active' : ''}`]];
476
-
477
- // Process tab content with the main markdown-it instance
478
- if (tab.content.trim()) {
479
- const tabContent = tab.content.trim();
480
-
481
- // Create a separate markdown-it instance for tab content to avoid double processing
482
- const tabMd = new MarkdownIt({
483
- html: true,
484
- linkify: true,
485
- typographer: true,
486
- breaks: true,
487
- highlight: function (str, lang) {
488
- // Handle mermaid diagrams in tabs
489
- if (lang === 'mermaid') {
490
- return '<pre class="mermaid">' + MarkdownIt.utils.escapeHtml(str) + '</pre>';
491
- }
492
-
493
- if (lang && hljs.getLanguage(lang)) {
494
- try {
495
- return '<pre class="hljs"><code>' +
496
- hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
497
- '</code></pre>';
498
- } catch (e) { console.error(`Error highlighting language ${lang}:`, e); }
499
- }
500
- return '<pre class="hljs"><code>' + MarkdownIt.utils.escapeHtml(str) + '</code></pre>';
501
- }
502
- });
503
-
504
- // Register the same plugins for the tab markdown instance
505
- tabMd.use(attrs, { leftDelimiter: '{', rightDelimiter: '}' });
506
- tabMd.use(markdown_it_footnote);
507
- tabMd.use(markdown_it_task_lists);
508
- tabMd.use(markdown_it_abbr);
509
- tabMd.use(markdown_it_deflist);
510
-
511
- // Register container renderers for the tab markdown instance
512
- Object.keys(containers).forEach(containerName => {
513
- const container = containers[containerName];
514
- tabMd.renderer.rules[`container_${containerName}_open`] = container.render;
515
- tabMd.renderer.rules[`container_${containerName}_close`] = container.render;
516
- });
517
-
518
- // Register the enhanced rules for the tab markdown instance
519
- tabMd.block.ruler.before('fence', 'enhanced_tabs', enhancedTabsRule, {
520
- alt: ['paragraph', 'reference', 'blockquote', 'list']
521
- });
522
- tabMd.block.ruler.before('paragraph', 'steps_container', stepsContainerRule, {
523
- alt: ['paragraph', 'reference', 'blockquote', 'list']
524
- });
525
- tabMd.block.ruler.before('paragraph', 'advanced_container', advancedContainerRule, {
526
- alt: ['paragraph', 'reference', 'blockquote', 'list']
527
- });
528
-
529
- // Register custom renderers for the tab markdown instance
530
- tabMd.renderer.rules.ordered_list_open = customOrderedListOpenRenderer;
531
- tabMd.renderer.rules.list_item_open = customListItemOpenRenderer;
532
- tabMd.renderer.rules.image = customImageRenderer;
533
-
534
- // Register tabs renderers for the tab markdown instance
535
- tabMd.renderer.rules.tabs_open = tabsOpenRenderer;
536
- tabMd.renderer.rules.tabs_nav_open = tabsNavOpenRenderer;
537
- tabMd.renderer.rules.tabs_nav_close = tabsNavCloseRenderer;
538
- tabMd.renderer.rules.tabs_nav_item = tabsNavItemRenderer;
539
- tabMd.renderer.rules.tabs_content_open = tabsContentOpenRenderer;
540
- tabMd.renderer.rules.tabs_content_close = tabsContentCloseRenderer;
541
- tabMd.renderer.rules.tab_pane_open = tabPaneOpenRenderer;
542
- tabMd.renderer.rules.tab_pane_close = tabPaneCloseRenderer;
543
- tabMd.renderer.rules.tabs_close = tabsCloseRenderer;
544
-
545
- // Register heading ID plugin for the tab markdown instance
546
- tabMd.use(headingIdPlugin);
547
-
548
- // Register standalone closing rule for the tab markdown instance
549
- tabMd.block.ruler.before('paragraph', 'standalone_closing', standaloneClosingRule, {
550
- alt: ['paragraph', 'reference', 'blockquote', 'list']
551
- });
552
-
553
- // Render the tab content
554
- const renderedContent = tabMd.render(tabContent);
555
- const htmlToken = state.push('html_block', '', 0);
556
- htmlToken.content = renderedContent;
557
- }
558
-
559
- state.push('tab_pane_close', 'div', -1);
560
- });
561
- state.push('tabs_content_close', 'div', -1);
562
- state.push('tabs_close', 'div', -1);
563
- state.line = nextLine + 1;
564
- return true;
60
+ return { frontmatter, htmlContent, headings };
565
61
  }
566
-
567
- // Add a rule to handle standalone closing tags
568
- const standaloneClosingRule = (state, startLine, endLine, silent) => {
569
- const start = state.bMarks[startLine] + state.tShift[startLine];
570
- const max = state.eMarks[startLine];
571
- const lineContent = state.src.slice(start, max).trim();
572
-
573
- if (lineContent === ':::') {
574
- if (silent) return true;
575
- // Skip this line by not creating any tokens
576
- state.line = startLine + 1;
577
- return true;
578
- }
579
-
580
- return false;
581
- };
582
-
583
- // Custom renderer for ordered lists in steps containers
584
- const customOrderedListOpenRenderer = function(tokens, idx, options, env, self) {
585
- const token = tokens[idx];
586
- // Check if we're inside a steps container by looking at the context
587
- let isInSteps = false;
588
-
589
- // Look back through tokens to see if we're in a steps container
590
- for (let i = idx - 1; i >= 0; i--) {
591
- if (tokens[i].type === 'container_steps_open') {
592
- isInSteps = true;
593
- break;
594
- }
595
- if (tokens[i].type === 'container_steps_close') {
596
- break;
597
- }
598
- }
599
62
 
600
- if (isInSteps) {
601
- const start = token.attrGet('start');
602
- return start ?
603
- `<ol class="steps-list" start="${start}">` :
604
- '<ol class="steps-list">';
605
- }
606
-
607
- // Default behavior for non-steps ordered lists
608
- const start = token.attrGet('start');
609
- return start ? `<ol start="${start}">` : '<ol>';
610
- };
611
-
612
- // Custom renderer for list items in steps containers
613
- const customListItemOpenRenderer = function(tokens, idx, options, env, self) {
614
- const token = tokens[idx];
615
- // Check if we're inside a steps container and this is a direct child
616
- let isInStepsList = false;
617
-
618
- // Look back through tokens to see if we're in a steps list
619
- for (let i = idx - 1; i >= 0; i--) {
620
- if (tokens[i].type === 'ordered_list_open' &&
621
- tokens[i].markup &&
622
- tokens[i].level < token.level) {
623
- // Check if this ordered list has steps-list class (meaning it's in steps container)
624
- let j = i - 1;
625
- while (j >= 0) {
626
- if (tokens[j].type === 'container_steps_open') {
627
- isInStepsList = true;
628
- break;
629
- }
630
- if (tokens[j].type === 'container_steps_close') {
631
- break;
632
- }
633
- j--;
634
- }
635
- break;
636
- }
637
- }
638
-
639
- if (isInStepsList) {
640
- return '<li class="step-item">';
641
- }
642
-
643
- // Default behavior for non-step list items
644
- return '<li>';
645
- };
646
-
647
- // Enhanced tabs renderers
648
- const tabsOpenRenderer = (tokens, idx) => {
649
- const token = tokens[idx];
650
- return `<div class="${token.attrs.map(attr => attr[1]).join(' ')}">`;
651
- };
652
-
653
- const tabsNavOpenRenderer = () => '<div class="docmd-tabs-nav">';
654
- const tabsNavCloseRenderer = () => '</div>';
655
-
656
- const tabsNavItemRenderer = (tokens, idx) => {
657
- const token = tokens[idx];
658
- return `<div class="${token.attrs[0][1]}">${token.content}</div>`;
659
- };
660
-
661
- const tabsContentOpenRenderer = () => '<div class="docmd-tabs-content">';
662
- const tabsContentCloseRenderer = () => '</div>';
663
-
664
- const tabPaneOpenRenderer = (tokens, idx) => {
665
- const token = tokens[idx];
666
- return `<div class="${token.attrs[0][1]}">`;
667
- };
668
-
669
- const tabPaneCloseRenderer = () => '</div>';
670
-
671
- const tabsCloseRenderer = () => '</div>';
672
-
673
- // Override the default image renderer to properly handle attributes like {.class}.
674
- const customImageRenderer = function(tokens, idx, options, env, self) {
675
- const defaultImageRenderer = function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); };
676
- const renderedImage = defaultImageRenderer(tokens, idx, options, env, self);
677
- const nextToken = tokens[idx + 1];
678
- if (nextToken && nextToken.type === 'attrs_block') {
679
- const attrs = nextToken.attrs || [];
680
- const attrsStr = attrs.map(([name, value]) => `${name}="${value}"`).join(' ');
681
- return renderedImage.replace('<img ', `<img ${attrsStr} `);
682
- }
683
- return renderedImage;
684
- };
685
-
686
- // Add IDs to headings for anchor links, used by the Table of Contents.
687
- const headingIdPlugin = (md) => {
688
- const originalHeadingOpen = md.renderer.rules.heading_open || function(tokens, idx, options, env, self) {
689
- return self.renderToken(tokens, idx, options);
690
- };
691
-
692
- md.renderer.rules.heading_open = function(tokens, idx, options, env, self) {
693
- const token = tokens[idx];
694
- const contentToken = tokens[idx + 1];
695
-
696
- if (contentToken && contentToken.type === 'inline' && contentToken.content) {
697
- const headingText = contentToken.content;
698
- const id = headingText
699
- .toLowerCase()
700
- .replace(/\s+/g, '-') // Replace spaces with -
701
- .replace(/[^\w-]+/g, '') // Remove all non-word chars
702
- .replace(/--+/g, '-') // Replace multiple - with single -
703
- .replace(/^-+/, '') // Trim - from start of text
704
- .replace(/-+$/, ''); // Trim - from end of text
705
-
706
- if (id) {
707
- token.attrSet('id', id);
708
- }
709
- }
710
-
711
- return originalHeadingOpen(tokens, idx, options, env, self);
712
- };
713
- };
714
-
715
- // ===================================================================
716
- // --- SAFE CONTAINER WRAPPER (FOR SIMPLE CONTAINERS) ---
717
- // The safeContainer function has been replaced by the advanced nested container system
718
- // which provides better nesting support and more robust parsing.
719
-
720
- // ===================================================================
721
- // --- ADVANCED NESTED CONTAINER SYSTEM IMPLEMENTATION ---
722
- // ===================================================================
723
-
724
- // The advanced nested container system is now implemented above
725
- // All containers (card, callout, button, steps, tabs) are handled by the new system
726
- // which supports seamless nesting of any container within any other container.
727
-
728
-
729
- // --- UTILITY AND PROCESSING FUNCTIONS ---
730
-
731
- function decodeHtmlEntities(html) {
732
- return html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
733
- }
734
-
735
- function extractHeadingsFromHtml(htmlContent) {
736
- const headings = [];
737
- const headingRegex = /<h([1-6])[^>]*?id="([^"]*)"[^>]*?>([\s\S]*?)<\/h\1>/g;
738
- let match;
739
- while ((match = headingRegex.exec(htmlContent)) !== null) {
740
- const level = parseInt(match[1], 10);
741
- const id = match[2];
742
- const text = decodeHtmlEntities(match[3].replace(/<\/?[^>]+(>|$)/g, ''));
743
- headings.push({ id, level, text });
744
- }
745
- return headings;
746
- }
747
-
748
- async function processMarkdownFile(filePath, md, config) {
749
- const rawContent = await fs.readFile(filePath, 'utf8');
750
- let frontmatter, markdownContent;
751
-
752
- try {
753
- ({ data: frontmatter, content: markdownContent } = matter(rawContent));
754
- } catch (e) {
755
- console.error(`❌ Error parsing frontmatter in ${formatPathForDisplay(filePath)}:`);
756
- console.error(` ${e.message}`);
757
- console.error(' This page will be skipped. Please fix the YAML syntax.');
758
- return null;
759
- }
760
-
761
- if (!frontmatter.title) {
762
- if (config.autoTitleFromH1 !== false) {
763
- const h1Match = markdownContent.match(/^#\s+(.*)/m);
764
- if (h1Match && h1Match[1]) {
765
- frontmatter.title = h1Match[1].trim();
766
- }
767
- }
768
- if (!frontmatter.title) {
769
- console.warn(`⚠️ Warning: Markdown file ${formatPathForDisplay(filePath)} has no title in frontmatter and no H1 fallback. The page header will be hidden.`);
770
- }
771
- }
772
-
773
- let htmlContent, headings;
774
- if (frontmatter.noStyle === true) {
775
- // For noStyle pages, NO markdown processing at all
776
- // Pass the raw content directly as-is
777
- htmlContent = markdownContent;
778
- headings = [];
779
- } else {
780
- htmlContent = md.render(markdownContent);
781
- headings = extractHeadingsFromHtml(htmlContent);
782
- }
783
-
784
- return {
785
- frontmatter,
786
- htmlContent,
787
- headings,
788
- };
789
- }
790
-
791
63
  async function findMarkdownFiles(dir) {
792
- let files = [];
793
- const items = await fs.readdir(dir, { withFileTypes: true });
794
- for (const item of items) {
795
- const fullPath = path.join(dir, item.name);
796
- if (item.isDirectory()) {
797
- files = files.concat(await findMarkdownFiles(fullPath));
798
- } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.markdown'))) {
799
- files.push(fullPath);
64
+ let files = [];
65
+ const items = await fs.readdir(dir, { withFileTypes: true });
66
+ for (const item of items) {
67
+ const fullPath = path.join(dir, item.name);
68
+ if (item.isDirectory()) {
69
+ files = files.concat(await findMarkdownFiles(fullPath));
70
+ } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.markdown'))) {
71
+ files.push(fullPath);
72
+ }
800
73
  }
801
- }
802
- return files;
74
+ return files;
803
75
  }
804
76
 
805
77
  module.exports = {