@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.
- package/README.md +2 -4
- package/assets/css/welcome.css +5 -377
- package/assets/images/preview-dark-1.webp +0 -0
- package/assets/images/preview-dark-2.webp +0 -0
- package/assets/images/preview-dark-3.webp +0 -0
- package/assets/images/preview-light-1.webp +0 -0
- package/assets/images/preview-light-2.webp +0 -0
- package/assets/images/preview-light-3.webp +0 -0
- package/config.js +40 -6
- package/docs/configuration.md +82 -7
- package/docs/content/containers/buttons.md +88 -0
- package/docs/content/containers/callouts.md +154 -0
- package/docs/content/containers/cards.md +93 -0
- package/docs/content/containers/index.md +35 -0
- package/docs/content/containers/nested-containers.md +329 -0
- package/docs/content/containers/steps.md +175 -0
- package/docs/content/containers/tabs.md +228 -0
- package/docs/content/custom-containers.md +19 -124
- package/docs/content/frontmatter.md +2 -2
- package/docs/content/no-style-example.md +2 -0
- package/docs/content/no-style-pages.md +52 -28
- package/docs/index.md +55 -27
- package/docs/plugins/seo.md +80 -31
- package/docs/theming/available-themes.md +17 -2
- package/docs/theming/light-dark-mode.md +12 -3
- package/package.json +21 -9
- package/src/assets/css/docmd-main.css +5 -806
- package/src/assets/css/docmd-theme-retro.css +9 -0
- package/src/assets/css/docmd-theme-ruby.css +7 -604
- package/src/assets/css/docmd-theme-sky.css +7 -649
- package/src/assets/js/docmd-image-lightbox.js +4 -2
- package/src/assets/js/docmd-main.js +157 -0
- package/src/commands/build.js +62 -120
- package/src/commands/dev.js +2 -1
- package/src/commands/init.js +23 -1
- package/src/core/config-loader.js +2 -0
- package/src/core/file-processor.js +669 -373
- package/src/core/html-generator.js +49 -40
- package/src/core/icon-renderer.js +3 -2
- package/src/plugins/analytics.js +5 -1
- package/src/plugins/seo.js +114 -62
- package/src/plugins/sitemap.js +6 -0
- package/src/templates/layout.ejs +40 -8
- package/src/templates/no-style.ejs +23 -6
- package/src/templates/partials/theme-init.js +26 -0
- package/assets/images/preview-dark-1.png +0 -0
- package/assets/images/preview-dark-2.png +0 -0
- package/assets/images/preview-dark-3.png +0 -0
- package/assets/images/preview-light-1.png +0 -0
- package/assets/images/preview-light-2.png +0 -0
- package/assets/images/preview-light-3.png +0 -0
- package/src/assets/js/docmd-theme-toggle.js +0 -59
|
@@ -1,461 +1,757 @@
|
|
|
1
|
-
//
|
|
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');
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
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
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
}
|
|
237
|
+
}
|
|
73
238
|
|
|
74
|
-
//
|
|
75
|
-
|
|
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
|
|
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
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
//
|
|
271
|
-
function
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
//
|
|
301
|
-
|
|
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
|
-
//
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
614
|
+
if (isInStepsList) {
|
|
615
|
+
return '<li class="step-item">';
|
|
616
|
+
}
|
|
308
617
|
|
|
309
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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(/&/g, '&')
|
|
320
|
-
.replace(/</g, '<')
|
|
321
|
-
.replace(/>/g, '>')
|
|
322
|
-
.replace(/"/g, '"')
|
|
323
|
-
.replace(/'/g, "'")
|
|
324
|
-
.replace(/ /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
|
-
|
|
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,
|
|
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
|
-
|
|
359
|
-
frontmatter = parsed.data;
|
|
360
|
-
markdownContent = parsed.content;
|
|
717
|
+
({ data: frontmatter, content: markdownContent } = matter(rawContent));
|
|
361
718
|
} catch (e) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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,
|
|
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
|
-
|
|
769
|
+
module.exports = {
|
|
770
|
+
processMarkdownFile,
|
|
771
|
+
createMarkdownItInstance,
|
|
476
772
|
extractHeadingsFromHtml,
|
|
477
|
-
findMarkdownFiles
|
|
773
|
+
findMarkdownFiles
|
|
478
774
|
};
|