@mgks/docmd 0.1.4 → 0.2.1

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.
Files changed (52) hide show
  1. package/README.md +2 -4
  2. package/assets/css/welcome.css +5 -377
  3. package/assets/images/preview-dark-1.webp +0 -0
  4. package/assets/images/preview-dark-2.webp +0 -0
  5. package/assets/images/preview-dark-3.webp +0 -0
  6. package/assets/images/preview-light-1.webp +0 -0
  7. package/assets/images/preview-light-2.webp +0 -0
  8. package/assets/images/preview-light-3.webp +0 -0
  9. package/config.js +40 -6
  10. package/docs/configuration.md +82 -7
  11. package/docs/content/containers/buttons.md +88 -0
  12. package/docs/content/containers/callouts.md +154 -0
  13. package/docs/content/containers/cards.md +93 -0
  14. package/docs/content/containers/index.md +35 -0
  15. package/docs/content/containers/nested-containers.md +329 -0
  16. package/docs/content/containers/steps.md +175 -0
  17. package/docs/content/containers/tabs.md +228 -0
  18. package/docs/content/custom-containers.md +19 -124
  19. package/docs/content/frontmatter.md +2 -2
  20. package/docs/content/no-style-example.md +2 -0
  21. package/docs/content/no-style-pages.md +52 -28
  22. package/docs/index.md +55 -27
  23. package/docs/plugins/seo.md +80 -31
  24. package/docs/theming/available-themes.md +17 -2
  25. package/docs/theming/light-dark-mode.md +12 -3
  26. package/package.json +21 -9
  27. package/src/assets/css/docmd-main.css +5 -806
  28. package/src/assets/css/docmd-theme-retro.css +9 -0
  29. package/src/assets/css/docmd-theme-ruby.css +7 -604
  30. package/src/assets/css/docmd-theme-sky.css +7 -649
  31. package/src/assets/js/docmd-image-lightbox.js +4 -2
  32. package/src/assets/js/docmd-main.js +157 -0
  33. package/src/commands/build.js +62 -120
  34. package/src/commands/dev.js +2 -1
  35. package/src/commands/init.js +23 -1
  36. package/src/core/config-loader.js +2 -0
  37. package/src/core/file-processor.js +669 -373
  38. package/src/core/html-generator.js +49 -40
  39. package/src/core/icon-renderer.js +3 -2
  40. package/src/plugins/analytics.js +5 -1
  41. package/src/plugins/seo.js +114 -62
  42. package/src/plugins/sitemap.js +6 -0
  43. package/src/templates/layout.ejs +40 -8
  44. package/src/templates/no-style.ejs +23 -6
  45. package/src/templates/partials/theme-init.js +26 -0
  46. package/assets/images/preview-dark-1.png +0 -0
  47. package/assets/images/preview-dark-2.png +0 -0
  48. package/assets/images/preview-dark-3.png +0 -0
  49. package/assets/images/preview-light-1.png +0 -0
  50. package/assets/images/preview-light-2.png +0 -0
  51. package/assets/images/preview-light-3.png +0 -0
  52. package/src/assets/js/docmd-theme-toggle.js +0 -59
@@ -1,461 +1,757 @@
1
- // src/core/file-processor.js
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
2
3
  const fs = require('fs-extra');
3
4
  const MarkdownIt = require('markdown-it');
4
5
  const matter = require('gray-matter');
5
6
  const hljs = require('highlight.js');
6
- const container = require('markdown-it-container');
7
7
  const attrs = require('markdown-it-attrs');
8
- const path = require('path'); // Add path module for findMarkdownFiles
8
+ 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');
9
13
 
10
- // Function to format paths for display (relative to CWD)
14
+ /**
15
+ * Formats an absolute path to be relative to the current working directory for cleaner logging.
16
+ */
11
17
  function formatPathForDisplay(absolutePath) {
12
18
  const CWD = process.cwd();
13
19
  const relativePath = path.relative(CWD, absolutePath);
14
-
15
- // If it's not a subdirectory, prefix with ./ for clarity
16
20
  if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
17
21
  return `./${relativePath}`;
18
22
  }
19
-
20
- // Return the relative path
21
23
  return relativePath;
22
24
  }
23
25
 
24
- const md = new MarkdownIt({
25
- html: true,
26
- linkify: true,
27
- typographer: true,
28
- highlight: function (str, lang) {
29
- if (lang && hljs.getLanguage(lang)) {
30
- try {
31
- return '<pre class="hljs"><code>' +
32
- hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
33
- '</code></pre>';
34
- } catch (e) {
35
- console.error(`Error highlighting language ${lang}:`, e);
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
+ if (lang && hljs.getLanguage(lang)) {
38
+ try {
39
+ return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
40
+ } catch (e) { console.error(`Highlighting error for lang ${lang}:`, e); }
41
+ }
42
+ return `<pre class="hljs"><code>${new MarkdownIt().utils.escapeHtml(str)}</code></pre>`;
43
+ };
44
+ }
45
+
46
+ const md = new MarkdownIt(mdOptions);
47
+
48
+ // --- Attach all plugins and rules to this instance ---
49
+ md.use(attrs, { leftDelimiter: '{', rightDelimiter: '}' });
50
+ md.use(markdown_it_footnote);
51
+ md.use(markdown_it_task_lists);
52
+ md.use(markdown_it_abbr);
53
+ md.use(markdown_it_deflist);
54
+
55
+ // Register renderers for all containers
56
+ Object.keys(containers).forEach(containerName => {
57
+ const container = containers[containerName];
58
+ md.renderer.rules[`container_${containerName}_open`] = container.render;
59
+ md.renderer.rules[`container_${containerName}_close`] = container.render;
60
+ });
61
+
62
+ // Register the enhanced rules
63
+ md.block.ruler.before('fence', 'steps_container', stepsContainerRule, {
64
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
65
+ });
66
+ md.block.ruler.before('fence', 'enhanced_tabs', enhancedTabsRule, {
67
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
68
+ });
69
+ md.block.ruler.before('paragraph', 'advanced_container', advancedContainerRule, {
70
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
71
+ });
72
+
73
+ // Register all custom renderers
74
+ md.renderer.rules.ordered_list_open = customOrderedListOpenRenderer;
75
+ md.renderer.rules.list_item_open = customListItemOpenRenderer;
76
+ md.renderer.rules.image = customImageRenderer;
77
+
78
+ // Register tabs renderers
79
+ md.renderer.rules.tabs_open = tabsOpenRenderer;
80
+ md.renderer.rules.tabs_nav_open = tabsNavOpenRenderer;
81
+ md.renderer.rules.tabs_nav_close = tabsNavCloseRenderer;
82
+ md.renderer.rules.tabs_nav_item = tabsNavItemRenderer;
83
+ md.renderer.rules.tabs_content_open = tabsContentOpenRenderer;
84
+ md.renderer.rules.tabs_content_close = tabsContentCloseRenderer;
85
+ md.renderer.rules.tab_pane_open = tabPaneOpenRenderer;
86
+ md.renderer.rules.tab_pane_close = tabPaneCloseRenderer;
87
+ md.renderer.rules.tabs_close = tabsCloseRenderer;
88
+
89
+ // Register heading ID plugin
90
+ md.use(headingIdPlugin);
91
+
92
+ // Register standalone closing rule
93
+ md.block.ruler.before('paragraph', 'standalone_closing', standaloneClosingRule, {
94
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
95
+ });
96
+
97
+ return md;
98
+ }
99
+
100
+ // ===================================================================
101
+ // --- ADVANCED NESTED CONTAINER SYSTEM ---
102
+ // ===================================================================
103
+
104
+ // Container definitions
105
+ // To add a new container type:
106
+ // 1. Add it to this containers object
107
+ // 2. Define the render function for opening (nesting === 1) and closing (nesting === -1)
108
+ // 3. The system will automatically register it and support nesting
109
+ const containers = {
110
+ card: {
111
+ name: 'card',
112
+ render: (tokens, idx) => {
113
+ if (tokens[idx].nesting === 1) {
114
+ const title = tokens[idx].info ? tokens[idx].info.trim() : '';
115
+ return `<div class="docmd-container card">${title ? `<div class="card-title">${title}</div>` : ''}<div class="card-content">`;
116
+ }
117
+ return '</div></div>';
118
+ }
119
+ },
120
+ callout: {
121
+ name: 'callout',
122
+ render: (tokens, idx) => {
123
+ if (tokens[idx].nesting === 1) {
124
+ const [type, ...titleParts] = tokens[idx].info.split(' ');
125
+ const title = titleParts.join(' ');
126
+ return `<div class="docmd-container callout callout-${type}">${title ? `<div class="callout-title">${title}</div>` : ''}<div class="callout-content">`;
127
+ }
128
+ return '</div></div>';
129
+ }
130
+ },
131
+ button: {
132
+ name: 'button',
133
+ selfClosing: true, // Mark as self-closing
134
+ render: (tokens, idx) => {
135
+ if (tokens[idx].nesting === 1) {
136
+ const parts = tokens[idx].info.split(' ');
137
+ const text = parts[0];
138
+ const url = parts[1];
139
+ const color = parts[2];
140
+ const colorStyle = color && color.startsWith('color:') ? ` style="background-color: ${color.split(':')[1]}"` : '';
141
+
142
+ // Check if URL starts with 'external:' for new tab behavior
143
+ let finalUrl = url;
144
+ let targetAttr = '';
145
+ if (url && url.startsWith('external:')) {
146
+ finalUrl = url.substring(9); // Remove 'external:' prefix
147
+ targetAttr = ' target="_blank" rel="noopener noreferrer"';
148
+ }
149
+
150
+ return `<a href="${finalUrl}" class="docmd-button"${colorStyle}${targetAttr}>${text.replace(/_/g, ' ')}</a>`;
36
151
  }
152
+ return '';
153
+ }
154
+ },
155
+ steps: {
156
+ name: 'steps',
157
+ render: (tokens, idx) => {
158
+ if (tokens[idx].nesting === 1) {
159
+ // Add a unique class for steps containers to enable CSS-based numbering reset
160
+ // The steps-numbering class will style only direct ol > li children as numbered steps
161
+ return '<div class="docmd-container steps steps-reset steps-numbering">';
162
+ }
163
+ return '</div>';
37
164
  }
38
- return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
39
165
  }
40
- });
41
-
42
- // Add markdown-it-attrs for image styling and other element attributes
43
- // This allows for {.class} syntax after elements
44
- md.use(attrs, {
45
- // Allow attributes on images and other elements
46
- leftDelimiter: '{',
47
- rightDelimiter: '}',
48
- allowedAttributes: ['class', 'id', 'width', 'height', 'style']
49
- });
50
-
51
- // Custom image renderer to ensure attributes are properly applied
52
- const defaultImageRenderer = md.renderer.rules.image;
53
- md.renderer.rules.image = function(tokens, idx, options, env, self) {
54
- // Get the rendered HTML from the default renderer
55
- const renderedImage = defaultImageRenderer(tokens, idx, options, env, self);
166
+ // Future containers can be added here:
167
+ // timeline: {
168
+ // name: 'timeline',
169
+ // render: (tokens, idx) => {
170
+ // if (tokens[idx].nesting === 1) {
171
+ // return '<div class="docmd-container timeline">';
172
+ // }
173
+ // return '</div>';
174
+ // }
175
+ // },
176
+ // changelog: {
177
+ // name: 'changelog',
178
+ // render: (tokens, idx) => {
179
+ // if (tokens[idx].nesting === 1) {
180
+ // return '<div class="docmd-container changelog">';
181
+ // }
182
+ // return '</div>';
183
+ // }
184
+ // }
185
+ };
186
+
187
+
188
+
189
+ // Advanced container rule with proper nesting support
190
+ function advancedContainerRule(state, startLine, endLine, silent) {
191
+ const start = state.bMarks[startLine] + state.tShift[startLine];
192
+ const max = state.eMarks[startLine];
193
+ const lineContent = state.src.slice(start, max).trim();
56
194
 
57
- // Check if the next token is an attrs_block
58
- const nextToken = tokens[idx + 1];
59
- if (nextToken && nextToken.type === 'attrs_block') {
60
- // Extract attributes from the attrs_block token
61
- const attrs = nextToken.attrs || [];
195
+ // Check if this is a container opening
196
+ const containerMatch = lineContent.match(/^:::\s*(\w+)(?:\s+(.+))?$/);
197
+ if (!containerMatch) return false;
198
+
199
+ const [, containerName, params] = containerMatch;
200
+ const container = containers[containerName];
201
+
202
+ if (!container) return false;
203
+
204
+ if (silent) return true;
205
+
206
+ // Handle self-closing containers (like buttons)
207
+ if (container.selfClosing) {
208
+ const openToken = state.push(`container_${containerName}_open`, 'div', 1);
209
+ openToken.info = params || '';
210
+ const closeToken = state.push(`container_${containerName}_close`, 'div', -1);
211
+ state.line = startLine + 1;
212
+ return true;
213
+ }
214
+
215
+ // Find the closing tag with proper nesting handling
216
+ let nextLine = startLine;
217
+ let found = false;
218
+ let depth = 1;
219
+
220
+ while (nextLine < endLine) {
221
+ nextLine++;
222
+ const nextStart = state.bMarks[nextLine] + state.tShift[nextLine];
223
+ const nextMax = state.eMarks[nextLine];
224
+ const nextContent = state.src.slice(nextStart, nextMax).trim();
62
225
 
63
- // Build the attributes string
64
- const attrsStr = attrs.map(([name, value]) => {
65
- if (name === 'class') {
66
- return `class="${value}"`;
67
- } else if (name.startsWith('data-')) {
68
- return `${name}="${value}"`;
69
- } else {
70
- return `${name}="${value}"`;
226
+ // Check for opening tags (any container)
227
+ if (nextContent.startsWith(':::')) {
228
+ const containerMatch = nextContent.match(/^:::\s*(\w+)/);
229
+ if (containerMatch && containerMatch[1] !== containerName) {
230
+ // Only increment depth for non-self-closing containers
231
+ const innerContainer = containers[containerMatch[1]];
232
+ if (innerContainer && innerContainer.render && !innerContainer.selfClosing) {
233
+ depth++;
234
+ }
235
+ continue;
71
236
  }
72
- }).join(' ');
237
+ }
73
238
 
74
- // Insert attributes into the image tag
75
- return renderedImage.replace('<img ', `<img ${attrsStr} `);
239
+ // Check for closing tags
240
+ if (nextContent === ':::') {
241
+ depth--;
242
+ if (depth === 0) {
243
+ found = true;
244
+ break;
245
+ }
246
+ }
76
247
  }
77
248
 
78
- return renderedImage;
79
- };
249
+ if (!found) return false;
250
+
251
+ // Create tokens
252
+ const openToken = state.push(`container_${containerName}_open`, 'div', 1);
253
+ openToken.info = params || '';
254
+
255
+ // Process content recursively
256
+ const oldParentType = state.parentType;
257
+ const oldLineMax = state.lineMax;
258
+
259
+ state.parentType = 'container';
260
+ state.lineMax = nextLine;
261
+
262
+ // Process the content inside the container
263
+ state.md.block.tokenize(state, startLine + 1, nextLine);
264
+
265
+ const closeToken = state.push(`container_${containerName}_close`, 'div', -1);
266
+
267
+ state.parentType = oldParentType;
268
+ state.lineMax = oldLineMax;
269
+ state.line = nextLine + 1;
270
+
271
+ return true;
272
+ }
80
273
 
81
- // Add anchors to headings for TOC linking
82
- md.use((md) => {
83
- // Original renderer
84
- const defaultRender = md.renderer.rules.heading_open || function(tokens, idx, options, env, self) {
85
- return self.renderToken(tokens, idx, options);
86
- };
274
+ // --- Simple Steps Container Rule ---
275
+ function stepsContainerRule(state, startLine, endLine, silent) {
276
+ const start = state.bMarks[startLine] + state.tShift[startLine];
277
+ const max = state.eMarks[startLine];
278
+ const lineContent = state.src.slice(start, max).trim();
279
+ if (lineContent !== '::: steps') return false;
280
+ if (silent) return true;
87
281
 
88
- md.renderer.rules.heading_open = function(tokens, idx, options, env, self) {
89
- const token = tokens[idx];
90
- // Get the heading level (h1, h2, etc.)
91
- const level = token.tag.substring(1);
282
+ // Find the closing ':::' for the steps container
283
+ let nextLine = startLine;
284
+ let found = false;
285
+ let depth = 1;
286
+
287
+ while (nextLine < endLine) {
288
+ nextLine++;
289
+ const nextStart = state.bMarks[nextLine] + state.tShift[nextLine];
290
+ const nextMax = state.eMarks[nextLine];
291
+ const nextContent = state.src.slice(nextStart, nextMax).trim();
92
292
 
93
- // Find the heading text from the next inline token
94
- const contentToken = tokens[idx + 1];
95
- if (contentToken && contentToken.type === 'inline') {
96
- const headingText = contentToken.content;
97
-
98
- // Generate an ID from the heading text
99
- // Simple slugify: lowercase, replace spaces and special chars with dashes
100
- const id = headingText
101
- .toLowerCase()
102
- .replace(/\s+/g, '-')
103
- .replace(/[^\w-]/g, '')
104
- .replace(/--+/g, '-')
105
- .replace(/^-+|-+$/g, '');
106
-
107
- // Add the id attribute
108
- if (id) {
109
- token.attrSet('id', id);
293
+ // Skip tab markers as they don't affect container depth
294
+ if (nextContent.startsWith('== tab')) {
295
+ continue;
296
+ }
297
+
298
+ // Check for opening tags (any container)
299
+ if (nextContent.startsWith(':::')) {
300
+ const containerMatch = nextContent.match(/^:::\s*(\w+)/);
301
+ if (containerMatch) {
302
+ const containerName = containerMatch[1];
303
+ // Only count non-self-closing containers for depth
304
+ const innerContainer = containers[containerName];
305
+ if (innerContainer && !innerContainer.selfClosing) {
306
+ depth++;
307
+ }
308
+ continue;
110
309
  }
111
310
  }
112
311
 
113
- // Call the original renderer
114
- return defaultRender(tokens, idx, options, env, self);
115
- };
116
- });
117
-
118
- // Custom Containers
119
- md.use(container, 'callout', {
120
- validate: function(params) {
121
- // Allows optional title for callout: ::: callout type [Optional Title Text]
122
- return params.trim().match(/^callout\s+(info|warning|tip|danger|success)(\s+.*)?$/);
123
- },
124
- render: function (tokens, idx) {
125
- const token = tokens[idx];
126
- const match = token.info.trim().match(/^callout\s+(info|warning|tip|danger|success)(\s+(.*))?$/);
127
-
128
- if (token.nesting === 1) {
129
- const type = match[1];
130
- const title = match[3] ? md.renderInline(match[3]) : ''; // Render title as markdown
131
- let titleHtml = '';
132
- if (title) {
133
- titleHtml = `<div class="callout-title">${title}</div>`;
312
+ // Check for closing tags
313
+ if (nextContent === ':::') {
314
+ depth--;
315
+ if (depth === 0) {
316
+ found = true;
317
+ break;
134
318
  }
135
- return `<div class="docmd-container callout callout-${type}">\n${titleHtml}<div class="callout-content">\n`;
136
- } else {
137
- return '</div></div>\n';
138
319
  }
139
320
  }
140
- });
321
+
322
+ if (!found) return false;
141
323
 
142
- md.use(container, 'card', {
143
- validate: function(params) {
144
- // Allows optional title for card: ::: card [Optional Title Text]
145
- return params.trim().match(/^card(\s+.*)?$/);
146
- },
147
- render: function (tokens, idx) {
148
- const token = tokens[idx];
149
- const titleText = token.info.trim().substring('card'.length).trim();
324
+ // Create tokens for steps container
325
+ const openToken = state.push('container_steps_open', 'div', 1);
326
+ openToken.info = '';
327
+
328
+ // Process content normally but disable automatic list processing
329
+ const oldParentType = state.parentType;
330
+ const oldLineMax = state.lineMax;
331
+
332
+ state.parentType = 'container';
333
+ state.lineMax = nextLine;
334
+
335
+ // Process the content inside the container
336
+ state.md.block.tokenize(state, startLine + 1, nextLine);
337
+
338
+ const closeToken = state.push('container_steps_close', 'div', -1);
339
+
340
+ state.parentType = oldParentType;
341
+ state.lineMax = oldLineMax;
342
+ state.line = nextLine + 1;
343
+
344
+ return true;
345
+ }
346
+
347
+ // --- Enhanced tabs rule with nested content support ---
348
+ function enhancedTabsRule(state, startLine, endLine, silent) {
349
+ const start = state.bMarks[startLine] + state.tShift[startLine];
350
+ const max = state.eMarks[startLine];
351
+ const lineContent = state.src.slice(start, max).trim();
150
352
 
151
- if (token.nesting === 1) {
152
- let titleHtml = '';
153
- if (titleText) {
154
- titleHtml = `<div class="card-title">${md.renderInline(titleText)}</div>\n`;
353
+ if (lineContent !== '::: tabs') return false;
354
+ if (silent) return true;
355
+
356
+ // Find the closing tag with proper nesting handling
357
+ let nextLine = startLine;
358
+ let found = false;
359
+ let depth = 1;
360
+ while (nextLine < endLine) {
361
+ nextLine++;
362
+ const nextStart = state.bMarks[nextLine] + state.tShift[nextLine];
363
+ const nextMax = state.eMarks[nextLine];
364
+ const nextContent = state.src.slice(nextStart, nextMax).trim();
365
+
366
+ // Check for opening tags (any container)
367
+ if (nextContent.startsWith(':::')) {
368
+ const containerMatch = nextContent.match(/^:::\s*(\w+)/);
369
+ if (containerMatch && containerMatch[1] !== 'tabs') {
370
+ // Don't increment depth for steps - they have their own depth counting
371
+ if (containerMatch[1] === 'steps') {
372
+ continue;
373
+ }
374
+ // Only increment depth for non-self-closing containers
375
+ const innerContainer = containers[containerMatch[1]];
376
+ if (innerContainer && !innerContainer.selfClosing) {
377
+ depth++;
378
+ }
379
+ continue;
380
+ }
381
+ }
382
+
383
+ // Check for closing tags
384
+ if (nextContent === ':::') {
385
+ depth--;
386
+ if (depth === 0) {
387
+ found = true;
388
+ break;
155
389
  }
156
- return `<div class="docmd-container card">\n${titleHtml}<div class="card-content">\n`;
157
- } else {
158
- return '</div></div>\n';
159
390
  }
160
391
  }
161
- });
162
-
163
- // Steps container: Uses CSS counters for H4 or P > STRONG elements within it.
164
- // Markdown syntax:
165
- // ::: steps
166
- // > 1. **First Step Title:**
167
- // > Content for step 1
168
- //
169
- // > 2. **Second Step Title:**
170
- // > More content
171
- // :::
172
- md.use(container, 'steps', {
173
- render: function (tokens, idx) {
174
- if (tokens[idx].nesting === 1) {
175
- // style="counter-reset: step-counter;" is added for CSS counters
176
- return '<div class="docmd-container steps" style="counter-reset: step-counter;">\n';
177
- } else {
178
- return '</div>\n';
392
+ if (!found) return false;
393
+
394
+ // Get the raw content by manually extracting lines
395
+ let content = '';
396
+ for (let i = startLine + 1; i < nextLine; i++) {
397
+ const lineStart = state.bMarks[i] + state.tShift[i];
398
+ const lineEnd = state.eMarks[i];
399
+ content += state.src.slice(lineStart, lineEnd) + '\n';
400
+ }
401
+
402
+ // Parse tabs manually
403
+ const lines = content.split('\n');
404
+ const tabs = [];
405
+ let currentTab = null;
406
+ let currentContent = [];
407
+
408
+ for (let i = 0; i < lines.length; i++) {
409
+ const line = lines[i].trim();
410
+ const tabMatch = line.match(/^==\s*tab\s+(?:"([^"]+)"|(\S+))$/);
411
+
412
+ if (tabMatch) {
413
+ // Save previous tab if exists
414
+ if (currentTab) {
415
+ currentTab.content = currentContent.join('\n').trim();
416
+ tabs.push(currentTab);
417
+ }
418
+ // Start new tab
419
+ const title = tabMatch[1] || tabMatch[2];
420
+ currentTab = { title: title, content: '' };
421
+ currentContent = [];
422
+ } else if (currentTab) {
423
+ // Add line to current tab content (only if not empty and not a tab marker)
424
+ if (lines[i].trim() && !lines[i].trim().startsWith('==')) {
425
+ currentContent.push(lines[i]);
426
+ }
179
427
  }
180
428
  }
181
- });
182
-
183
- // Post-process step markers for blockquote-based steps
184
- function processStepsContent(html) {
185
- // Clean up any malformed containers
186
- html = html.replace(/<blockquote>\s*<p>::: /g, '<p>');
187
-
188
- // Find all steps containers and process their blockquotes as steps
189
- return html.replace(
190
- /<div class="docmd-container steps"[^>]*>([\s\S]*?)<\/div>/g,
191
- function(match, stepsContent) {
192
- // Process blockquotes within steps container
193
- const processedContent = stepsContent
194
- // Handle numbered steps - improved pattern to better capture the number and title
195
- .replace(
196
- /<blockquote>\s*<[^>]*>\s*(\d+|[*])(?:\.)?(?:\s*)(?:<strong>)?([^<]*)(?:<\/strong>)?(?::)?(?:\s*)([\s\S]*?)(?=<\/blockquote>)/g,
197
- function(blockquote, stepNumber, stepTitle, stepContent) {
198
- // Ensure there's always content in the step title
199
- const title = stepTitle.trim() || `Step ${stepNumber}`;
200
-
201
- // Preserve paragraph breaks in the content
202
- const formattedContent = stepContent
203
- .replace(/<p>([\s\S]*?)<\/p>/g, '</div><p>$1</p><div class="step-content">') // Convert paragraphs
204
- .replace(/<pre([\s\S]*?)<\/pre>/g, '</div><pre$1</pre><div class="step-content">'); // Preserve code blocks
205
-
206
- return `<div class="step"><h4>${stepNumber}. <strong>${title}</strong></h4><div class="step-content">${formattedContent}</div></div>`;
207
- }
208
- )
209
- // Handle unnumbered steps - like "**Title:**"
210
- .replace(
211
- /<blockquote>\s*<p><strong>([^<:]*?):<\/strong>([\s\S]*?)(?=<\/blockquote>)/g,
212
- function(blockquote, stepTitle, stepContent) {
213
- // Preserve paragraph breaks in the content
214
- const formattedContent = stepContent
215
- .replace(/<p>([\s\S]*?)<\/p>/g, '</div><p>$1</p><div class="step-content">') // Convert paragraphs
216
- .replace(/<pre([\s\S]*?)<\/pre>/g, '</div><pre$1</pre><div class="step-content">'); // Preserve code blocks
217
-
218
- return `<div class="step"><h4><strong>${stepTitle}</strong></h4><div class="step-content">${formattedContent}</div></div>`;
219
- }
220
- )
221
- // Handle any remaining blockquotes as generic steps
222
- .replace(
223
- /<blockquote>([\s\S]*?)<\/blockquote>/g,
224
- function(blockquote, content) {
225
- return `<div class="step">${content}</div>`;
429
+
430
+ // Save the last tab
431
+ if (currentTab) {
432
+ currentTab.content = currentContent.join('\n').trim();
433
+ tabs.push(currentTab);
434
+ }
435
+
436
+ // Create tabs structure
437
+ const openToken = state.push('tabs_open', 'div', 1);
438
+ openToken.attrs = [['class', 'docmd-tabs']];
439
+
440
+ // Create navigation
441
+ const navToken = state.push('tabs_nav_open', 'div', 1);
442
+ navToken.attrs = [['class', 'docmd-tabs-nav']];
443
+ tabs.forEach((tab, index) => {
444
+ const navItemToken = state.push('tabs_nav_item', 'div', 0);
445
+ navItemToken.attrs = [['class', `docmd-tabs-nav-item ${index === 0 ? 'active' : ''}`]];
446
+ navItemToken.content = tab.title;
447
+ });
448
+ state.push('tabs_nav_close', 'div', -1);
449
+
450
+ // Create content
451
+ const contentToken = state.push('tabs_content_open', 'div', 1);
452
+ contentToken.attrs = [['class', 'docmd-tabs-content']];
453
+ tabs.forEach((tab, index) => {
454
+ const paneToken = state.push('tab_pane_open', 'div', 1);
455
+ paneToken.attrs = [['class', `docmd-tab-pane ${index === 0 ? 'active' : ''}`]];
456
+
457
+ // Process tab content with the main markdown-it instance
458
+ if (tab.content.trim()) {
459
+ const tabContent = tab.content.trim();
460
+
461
+ // Create a separate markdown-it instance for tab content to avoid double processing
462
+ const tabMd = new MarkdownIt({
463
+ html: true,
464
+ linkify: true,
465
+ typographer: true,
466
+ breaks: true,
467
+ highlight: function (str, lang) {
468
+ if (lang && hljs.getLanguage(lang)) {
469
+ try {
470
+ return '<pre class="hljs"><code>' +
471
+ hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
472
+ '</code></pre>';
473
+ } catch (e) { console.error(`Error highlighting language ${lang}:`, e); }
226
474
  }
227
- );
475
+ return '<pre class="hljs"><code>' + MarkdownIt.utils.escapeHtml(str) + '</code></pre>';
476
+ }
477
+ });
228
478
 
229
- // Fix any empty step-content divs or doubled divs
230
- let fixedContent = processedContent
231
- .replace(/<div class="step-content"><\/div><div class="step-content">/g, '<div class="step-content">')
232
- .replace(/<div class="step-content"><\/div>/g, '');
479
+ // Register the same plugins for the tab markdown instance
480
+ tabMd.use(attrs, { leftDelimiter: '{', rightDelimiter: '}' });
481
+ tabMd.use(markdown_it_footnote);
482
+ tabMd.use(markdown_it_task_lists);
483
+ tabMd.use(markdown_it_abbr);
484
+ tabMd.use(markdown_it_deflist);
233
485
 
234
- return `<div class="docmd-container steps" style="counter-reset: step-counter;">${fixedContent}</div>`;
235
- }
236
- );
237
- }
238
-
239
- // Pre-process step markers in Markdown content
240
- // to ensure they'll be processed correctly by the markdown renderer
241
- function preprocessStepMarkers(content) {
242
- // Find content between ::: steps and ::: markers
243
- return content.replace(
244
- /:::\s*steps\s*\n([\s\S]*?):::/g,
245
- function(match, stepsContent) {
246
- // Replace the step markers with a format that will survive markdown parsing
247
- const processedSteps = stepsContent.replace(
248
- /^::\s*((?:\d+|\*)?\.?\s*)(.*)$/gm,
249
- function(stepMatch, stepNumber, stepContent) {
250
- // Format it as a heading that we can target later
251
- return `### STEP_MARKER ${stepNumber}${stepContent}`;
252
- }
253
- );
486
+ // Register container renderers for the tab markdown instance
487
+ Object.keys(containers).forEach(containerName => {
488
+ const container = containers[containerName];
489
+ tabMd.renderer.rules[`container_${containerName}_open`] = container.render;
490
+ tabMd.renderer.rules[`container_${containerName}_close`] = container.render;
491
+ });
492
+
493
+ // Register the enhanced rules for the tab markdown instance
494
+ tabMd.block.ruler.before('fence', 'enhanced_tabs', enhancedTabsRule, {
495
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
496
+ });
497
+ tabMd.block.ruler.before('paragraph', 'steps_container', stepsContainerRule, {
498
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
499
+ });
500
+ tabMd.block.ruler.before('paragraph', 'advanced_container', advancedContainerRule, {
501
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
502
+ });
503
+
504
+ // Register custom renderers for the tab markdown instance
505
+ tabMd.renderer.rules.ordered_list_open = customOrderedListOpenRenderer;
506
+ tabMd.renderer.rules.list_item_open = customListItemOpenRenderer;
507
+ tabMd.renderer.rules.image = customImageRenderer;
254
508
 
255
- return `::: steps\n${processedSteps}:::`;
509
+ // Register tabs renderers for the tab markdown instance
510
+ tabMd.renderer.rules.tabs_open = tabsOpenRenderer;
511
+ tabMd.renderer.rules.tabs_nav_open = tabsNavOpenRenderer;
512
+ tabMd.renderer.rules.tabs_nav_close = tabsNavCloseRenderer;
513
+ tabMd.renderer.rules.tabs_nav_item = tabsNavItemRenderer;
514
+ tabMd.renderer.rules.tabs_content_open = tabsContentOpenRenderer;
515
+ tabMd.renderer.rules.tabs_content_close = tabsContentCloseRenderer;
516
+ tabMd.renderer.rules.tab_pane_open = tabPaneOpenRenderer;
517
+ tabMd.renderer.rules.tab_pane_close = tabPaneCloseRenderer;
518
+ tabMd.renderer.rules.tabs_close = tabsCloseRenderer;
519
+
520
+ // Register heading ID plugin for the tab markdown instance
521
+ tabMd.use(headingIdPlugin);
522
+
523
+ // Register standalone closing rule for the tab markdown instance
524
+ tabMd.block.ruler.before('paragraph', 'standalone_closing', standaloneClosingRule, {
525
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
526
+ });
527
+
528
+ // Render the tab content
529
+ const renderedContent = tabMd.render(tabContent);
530
+ const htmlToken = state.push('html_block', '', 0);
531
+ htmlToken.content = renderedContent;
256
532
  }
257
- );
533
+
534
+ state.push('tab_pane_close', 'div', -1);
535
+ });
536
+ state.push('tabs_content_close', 'div', -1);
537
+ state.push('tabs_close', 'div', -1);
538
+ state.line = nextLine + 1;
539
+ return true;
258
540
  }
259
541
 
260
- // Post-process step markers back to the expected format
261
- function postprocessStepMarkers(html) {
262
- return html.replace(
263
- /<h3>STEP_MARKER\s*((?:\d+|\*)?\.?\s*)(.*?)<\/h3>/g,
264
- function(match, stepNumber, stepContent) {
265
- return `<h4>${stepNumber}${stepContent}</h4>`;
266
- }
267
- );
268
- }
542
+ // Add a rule to handle standalone closing tags
543
+ const standaloneClosingRule = (state, startLine, endLine, silent) => {
544
+ const start = state.bMarks[startLine] + state.tShift[startLine];
545
+ const max = state.eMarks[startLine];
546
+ const lineContent = state.src.slice(start, max).trim();
547
+
548
+ if (lineContent === ':::') {
549
+ if (silent) return true;
550
+ // Skip this line by not creating any tokens
551
+ state.line = startLine + 1;
552
+ return true;
553
+ }
554
+
555
+ return false;
556
+ };
269
557
 
270
- // Escape container syntax in code blocks
271
- function escapeContainerSyntax(content) {
272
- // Find all fenced code blocks and escape container markers within them
273
- return content.replace(
274
- /```(.*?)\n([\s\S]*?)```/g,
275
- function(match, language, codeContent) {
276
- // Don't modify code blocks that already contain escaped markers
277
- if (codeContent.includes("\\:::") || codeContent.includes("\\::")) {
278
- return match;
279
- }
280
-
281
- // Escape ::: and :: markers within code blocks, but use a special marker
282
- // that won't render a backslash in the final output
283
- const escapedContent = codeContent
284
- .replace(/:::/g, "___DOCMD_CONTAINER_ESCAPED___:::")
285
- .replace(/^::/gm, "___DOCMD_CONTAINER_ESCAPED___::");
286
-
287
- return "```" + language + "\n" + escapedContent + "```";
558
+ // Custom renderer for ordered lists in steps containers
559
+ const customOrderedListOpenRenderer = function(tokens, idx, options, env, self) {
560
+ const token = tokens[idx];
561
+ // Check if we're inside a steps container by looking at the context
562
+ let isInSteps = false;
563
+
564
+ // Look back through tokens to see if we're in a steps container
565
+ for (let i = idx - 1; i >= 0; i--) {
566
+ if (tokens[i].type === 'container_steps_open') {
567
+ isInSteps = true;
568
+ break;
288
569
  }
289
- );
290
- }
291
-
292
- // Fix container syntax issues in Markdown content
293
- function normalizeContainerSyntax(content) {
294
- // 1. Ensure container opening markers are at the beginning of lines, not inline
295
- let fixed = content.replace(/([^\n])(:::)/g, '$1\n$2');
570
+ if (tokens[i].type === 'container_steps_close') {
571
+ break;
572
+ }
573
+ }
296
574
 
297
- // 2. Ensure container closing markers are at the beginning of lines and have proper newlines
298
- fixed = fixed.replace(/(:::)([^\n])/g, '$1\n$2');
575
+ if (isInSteps) {
576
+ const start = token.attrGet('start');
577
+ return start ?
578
+ `<ol class="steps-list" start="${start}">` :
579
+ '<ol class="steps-list">';
580
+ }
299
581
 
300
- // 3. Fix extra spaces after container marker
301
- fixed = fixed.replace(/:::\s+(\w+)/g, '::: $1');
582
+ // Default behavior for non-steps ordered lists
583
+ const start = token.attrGet('start');
584
+ return start ? `<ol start="${start}">` : '<ol>';
585
+ };
586
+
587
+ // Custom renderer for list items in steps containers
588
+ const customListItemOpenRenderer = function(tokens, idx, options, env, self) {
589
+ const token = tokens[idx];
590
+ // Check if we're inside a steps container and this is a direct child
591
+ let isInStepsList = false;
302
592
 
303
- // 4. Fix container markers that have newlines within them
304
- fixed = fixed.replace(/:::\n(\w+)/g, '::: $1');
593
+ // Look back through tokens to see if we're in a steps list
594
+ for (let i = idx - 1; i >= 0; i--) {
595
+ if (tokens[i].type === 'ordered_list_open' &&
596
+ tokens[i].markup &&
597
+ tokens[i].level < token.level) {
598
+ // Check if this ordered list has steps-list class (meaning it's in steps container)
599
+ let j = i - 1;
600
+ while (j >= 0) {
601
+ if (tokens[j].type === 'container_steps_open') {
602
+ isInStepsList = true;
603
+ break;
604
+ }
605
+ if (tokens[j].type === 'container_steps_close') {
606
+ break;
607
+ }
608
+ j--;
609
+ }
610
+ break;
611
+ }
612
+ }
305
613
 
306
- // 5. Fix missing spaces between ::: and container type
307
- fixed = fixed.replace(/:::(\w+)/g, '::: $1');
614
+ if (isInStepsList) {
615
+ return '<li class="step-item">';
616
+ }
308
617
 
309
- return fixed;
618
+ // Default behavior for non-step list items
619
+ return '<li>';
620
+ };
621
+
622
+ // Enhanced tabs renderers
623
+ const tabsOpenRenderer = (tokens, idx) => {
624
+ const token = tokens[idx];
625
+ return `<div class="${token.attrs.map(attr => attr[1]).join(' ')}">`;
626
+ };
627
+
628
+ const tabsNavOpenRenderer = () => '<div class="docmd-tabs-nav">';
629
+ const tabsNavCloseRenderer = () => '</div>';
630
+
631
+ const tabsNavItemRenderer = (tokens, idx) => {
632
+ const token = tokens[idx];
633
+ return `<div class="${token.attrs[0][1]}">${token.content}</div>`;
634
+ };
635
+
636
+ const tabsContentOpenRenderer = () => '<div class="docmd-tabs-content">';
637
+ const tabsContentCloseRenderer = () => '</div>';
638
+
639
+ const tabPaneOpenRenderer = (tokens, idx) => {
640
+ const token = tokens[idx];
641
+ return `<div class="${token.attrs[0][1]}">`;
642
+ };
643
+
644
+ const tabPaneCloseRenderer = () => '</div>';
645
+
646
+ const tabsCloseRenderer = () => '</div>';
647
+
648
+ // Override the default image renderer to properly handle attributes like {.class}.
649
+ const customImageRenderer = function(tokens, idx, options, env, self) {
650
+ const defaultImageRenderer = function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); };
651
+ const renderedImage = defaultImageRenderer(tokens, idx, options, env, self);
652
+ const nextToken = tokens[idx + 1];
653
+ if (nextToken && nextToken.type === 'attrs_block') {
654
+ const attrs = nextToken.attrs || [];
655
+ const attrsStr = attrs.map(([name, value]) => `${name}="${value}"`).join(' ');
656
+ return renderedImage.replace('<img ', `<img ${attrsStr} `);
657
+ }
658
+ return renderedImage;
659
+ };
660
+
661
+ // Add IDs to headings for anchor links, used by the Table of Contents.
662
+ const headingIdPlugin = (md) => {
663
+ const defaultRender = function(tokens, idx, options, env, self) {
664
+ return self.renderToken(tokens, idx, options);
665
+ };
666
+ const headingOpenRenderer = function(tokens, idx, options, env, self) {
667
+ const token = tokens[idx];
668
+ const contentToken = tokens[idx + 1];
669
+ if (contentToken && contentToken.type === 'inline') {
670
+ const headingText = contentToken.content;
671
+ const id = headingText.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '').replace(/--+/g, '-').replace(/^-+|-+$/g, '');
672
+ if (id) { token.attrSet('id', id); }
673
+ }
674
+ return defaultRender(tokens, idx, options, env, self);
675
+ }
310
676
  }
311
677
 
312
- /**
313
- * Decodes HTML entities in a string
314
- * @param {string} html - The HTML string to decode
315
- * @returns {string} - Decoded string
316
- */
678
+
679
+ // ===================================================================
680
+ // --- SAFE CONTAINER WRAPPER (FOR SIMPLE CONTAINERS) ---
681
+ // The safeContainer function has been replaced by the advanced nested container system
682
+ // which provides better nesting support and more robust parsing.
683
+
684
+ // ===================================================================
685
+ // --- ADVANCED NESTED CONTAINER SYSTEM IMPLEMENTATION ---
686
+ // ===================================================================
687
+
688
+ // The advanced nested container system is now implemented above
689
+ // All containers (card, callout, button, steps, tabs) are handled by the new system
690
+ // which supports seamless nesting of any container within any other container.
691
+
692
+
693
+ // --- UTILITY AND PROCESSING FUNCTIONS ---
694
+
317
695
  function decodeHtmlEntities(html) {
318
- return html
319
- .replace(/&amp;/g, '&')
320
- .replace(/&lt;/g, '<')
321
- .replace(/&gt;/g, '>')
322
- .replace(/&quot;/g, '"')
323
- .replace(/&#39;/g, "'")
324
- .replace(/&nbsp;/g, ' ');
696
+ return html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
325
697
  }
326
698
 
327
- /**
328
- * Extracts headings from HTML content for table of contents generation
329
- * @param {string} htmlContent - The rendered HTML content
330
- * @returns {Array} - Array of heading objects with id, level, and text
331
- */
332
699
  function extractHeadingsFromHtml(htmlContent) {
333
700
  const headings = [];
334
-
335
- // Regular expression to find heading tags (h1-h6) with their content and id attributes
336
701
  const headingRegex = /<h([1-6])[^>]*?id="([^"]*)"[^>]*?>([\s\S]*?)<\/h\1>/g;
337
-
338
702
  let match;
339
703
  while ((match = headingRegex.exec(htmlContent)) !== null) {
340
704
  const level = parseInt(match[1], 10);
341
705
  const id = match[2];
342
- // Remove any HTML tags inside the heading text
343
- const textWithTags = match[3].replace(/<\/?[^>]+(>|$)/g, '');
344
- // Decode any HTML entities in the text
345
- const text = decodeHtmlEntities(textWithTags);
346
-
706
+ const text = decodeHtmlEntities(match[3].replace(/<\/?[^>]+(>|$)/g, ''));
347
707
  headings.push({ id, level, text });
348
708
  }
349
-
350
709
  return headings;
351
710
  }
352
711
 
353
- async function processMarkdownFile(filePath, options = { isDev: false }) {
712
+ async function processMarkdownFile(filePath, md, config) {
354
713
  const rawContent = await fs.readFile(filePath, 'utf8');
355
714
  let frontmatter, markdownContent;
356
715
 
357
716
  try {
358
- const parsed = matter(rawContent);
359
- frontmatter = parsed.data;
360
- markdownContent = parsed.content;
717
+ ({ data: frontmatter, content: markdownContent } = matter(rawContent));
361
718
  } catch (e) {
362
- if (e.name === 'YAMLException') {
363
- // Provide more specific error for YAML parsing issues
364
- const errorMessage = `Error parsing YAML frontmatter in ${formatPathForDisplay(filePath)}: ${e.reason || e.message}${e.mark ? ` at line ${e.mark.line + 1}, column ${e.mark.column + 1}` : ''}. Please check the syntax.`;
365
- console.error(`❌ ${errorMessage}`);
366
- throw new Error(errorMessage); // Propagate error to stop build/dev
367
- }
368
- // For other errors from gray-matter or unknown errors
369
- console.error(`❌ Error processing frontmatter in ${formatPathForDisplay(filePath)}: ${e.message}`);
370
- throw e;
719
+ console.error(`❌ Error parsing frontmatter in ${formatPathForDisplay(filePath)}:`);
720
+ console.error(` ${e.message}`);
721
+ console.error(' This page will be skipped. Please fix the YAML syntax.');
722
+ return null;
371
723
  }
372
724
 
373
725
  if (!frontmatter.title) {
374
- console.warn(`⚠️ Warning: Markdown file ${formatPathForDisplay(filePath)} is missing a 'title' in its frontmatter. Using filename as fallback.`);
375
- // Fallback title, or you could make it an error
376
- // frontmatter.title = path.basename(filePath, path.extname(filePath));
377
- }
378
-
379
- // Special handling for no-style pages with HTML content
380
- if (frontmatter.noStyle === true) {
381
- // Only log when not in dev mode to reduce console output during dev
382
- if (!options.isDev) {
383
- console.log(`📄 Processing no-style page: ${formatPathForDisplay(filePath)} - Using raw HTML content`);
726
+ if (config.autoTitleFromH1 !== false) {
727
+ const h1Match = markdownContent.match(/^#\s+(.*)/m);
728
+ if (h1Match && h1Match[1]) {
729
+ frontmatter.title = h1Match[1].trim();
730
+ }
731
+ }
732
+ if (!frontmatter.title) {
733
+ console.warn(`⚠️ Warning: Markdown file ${formatPathForDisplay(filePath)} has no title in frontmatter and no H1 fallback. The page header will be hidden.`);
384
734
  }
385
-
386
- // For no-style pages, we'll use the raw content directly
387
- // No markdown processing, no HTML escaping
388
- const htmlContent = markdownContent;
389
-
390
- // Extract headings for table of contents (if needed)
391
- const headings = extractHeadingsFromHtml(htmlContent);
392
-
393
- return {
394
- frontmatter,
395
- htmlContent,
396
- headings,
397
- };
398
735
  }
399
736
 
400
- // Regular processing for standard pages
401
- // Check if this is a documentation example showing how to use containers
402
- const isContainerDocumentation = markdownContent.includes('containerName [optionalTitleOrType]') ||
403
- markdownContent.includes('## Callouts') ||
404
- markdownContent.includes('## Cards') ||
405
- markdownContent.includes('## Steps');
406
-
407
- // Special handling for container documentation - escape container syntax in code blocks
408
- if (isContainerDocumentation) {
409
- markdownContent = escapeContainerSyntax(markdownContent);
737
+ let htmlContent, headings;
738
+ if (frontmatter.noStyle === true) {
739
+ // For noStyle pages, NO markdown processing at all
740
+ // Pass the raw content directly as-is
741
+ htmlContent = markdownContent;
742
+ headings = [];
743
+ } else {
744
+ htmlContent = md.render(markdownContent);
745
+ headings = extractHeadingsFromHtml(htmlContent);
410
746
  }
411
-
412
- // Normalize container syntax
413
- const normalizedContent = normalizeContainerSyntax(markdownContent);
414
-
415
- // Render to HTML
416
- let htmlContent = md.render(normalizedContent);
417
-
418
- // Apply steps formatting
419
- htmlContent = processStepsContent(htmlContent);
420
-
421
- // Fix any specific issues
422
- // 1. Fix the issue with "These custom containers" paragraph in custom-containers.md
423
- htmlContent = htmlContent.replace(
424
- /<p>You should see "Application started successfully!" in your console.\s*<\/p>\s*<p>::: These custom containers/,
425
- '<p>You should see "Application started successfully!" in your console.</p></div><p>These custom containers'
426
- );
427
-
428
- // 2. Fix any remaining ::: markers at the start of paragraphs
429
- htmlContent = htmlContent.replace(/<p>:::\s+(.*?)<\/p>/g, '<p>$1</p>');
430
-
431
- // 3. Fix any broken Asterisk steps
432
- htmlContent = htmlContent.replace(
433
- /<div class="step"><h4>\*\. <strong><\/strong><\/h4>(.+?)<\/strong>/,
434
- '<div class="step"><h4>*. <strong>$1</strong>'
435
- );
436
-
437
- // 4. Replace our special escape marker with nothing (to fix backslash issue in rendered HTML)
438
- htmlContent = htmlContent.replace(/___DOCMD_CONTAINER_ESCAPED___/g, '');
439
-
440
- // Extract headings for table of contents
441
- const headings = extractHeadingsFromHtml(htmlContent);
442
747
 
443
748
  return {
444
- frontmatter: {
445
- title: "Untitled Page", // Default if not provided and no fallback
446
- ...frontmatter
447
- },
749
+ frontmatter,
448
750
  htmlContent,
449
- headings, // Add headings to the returned object
751
+ headings,
450
752
  };
451
753
  }
452
754
 
453
- // Add findMarkdownFiles function
454
- /**
455
- * Recursively finds all Markdown files in a directory and its subdirectories
456
- * @param {string} dir - Directory to search in
457
- * @returns {Promise<string[]>} - Array of file paths
458
- */
459
755
  async function findMarkdownFiles(dir) {
460
756
  let files = [];
461
757
  const items = await fs.readdir(dir, { withFileTypes: true });
@@ -470,9 +766,9 @@ async function findMarkdownFiles(dir) {
470
766
  return files;
471
767
  }
472
768
 
473
- module.exports = {
474
- processMarkdownFile,
475
- mdInstance: md,
769
+ module.exports = {
770
+ processMarkdownFile,
771
+ createMarkdownItInstance,
476
772
  extractHeadingsFromHtml,
477
- findMarkdownFiles // Export the findMarkdownFiles function
773
+ findMarkdownFiles
478
774
  };