@dogsbay/adoc2md-modular 0.2.0-beta.48

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/dist/engine.js ADDED
@@ -0,0 +1,2075 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const ADMONS = JSON.parse('{"CAUTION":"\ud83d\udd25","IMPORTANT":"\u2757","NOTE":"\ud83d\udccc","TIP":"\ud83d\udca1","WARNING":"\u26a0\ufe0f"}');
4
+ // Map AsciiDoc admonition types to lowercase for markdown formats (MkDocs, Docusaurus)
5
+ const ADMON_TYPES = { NOTE: 'note', WARNING: 'warning', TIP: 'tip', IMPORTANT: 'important', CAUTION: 'caution' };
6
+ const ATTRIBUTES = JSON.parse('{"empty":"","idprefix":"_","idseparator":"_","markdown-line-break":"\\\\","markdown-strikethrough":"~~",' +
7
+ '"nbsp":"&#160;","quotes":"<q> </q>","sp":" ","vbar":"|","zwsp":"&#8203;"}');
8
+ const BREAKS = { "'''": '---', '***': '---', '---': '---', '<<<': undefined, 'toc::[]': undefined };
9
+ const CONUMS = [...Array(19)].reduce((obj, _, i) => (obj[i + 1] = String.fromCharCode(0x2460 + i)) && obj, {});
10
+ const DELIMS = { '----': 'v', '-----': 'v', '....': 'v', '====': 'c', '|===': 't', '--': 'c', '****': 'c', ____: 'c', '++++': 'p' };
11
+ const LIST_MARKERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, '*', '.', '<', '-'].reduce((obj, c) => (obj['' + c] = true) && obj, {});
12
+ const NORMAL_SUBS = ['quotes', 'attributes', 'macros'];
13
+ const SUBSTITUTORS = { quotes, attributes, macros, callouts };
14
+ const TDIV = { '': '| --- ', '<': '| :-- ', '^': '| :-: ', '>': '| --: ' };
15
+ const AttributeEntryRx = /^:(!)?([^:-][^:]*):(?:$| (.+))/;
16
+ const AttributeRefRx = /(\\)?\{([\p{L}\d_][\p{L}\d_-]*)\}/gu;
17
+ const AuthorInfoLineRx = /^(?:[\p{Alpha}\d_]+(?: +[\p{Alpha}\d_]+){0,2}(?: +<([^>]+)>)?(?:; |$))+$/u;
18
+ const BlockAnchorRx = /^\[([\p{L}_][\p{Alpha}\d_\-:.]*)(?:, ?(.+))?\]$/u;
19
+ const BlockImageMacroRx = /^image::([^\s[][^[]*)\[(.*)\]$/;
20
+ const CellDelimiterRx = /(?:(?:^| +)(?:[<>^.]*[a-z]?)|)\| */;
21
+ const ConumRx = /(^| )<([.1-9]|1\d)>(?=(?: <(?:[.1-9]|1\d)>)*$)/g;
22
+ const DlistItemRx = /^(?!\/\/)(\S.*?)(:{2,4})(?: (.+))?\s*($)/;
23
+ const ElementAttributeRx = /(?:^|, *)(?:(\w[\w-]*)=)?(?:("|')([^\2]+?)\2|([^,]+|))/g;
24
+ const EmphasisSpanMetaRx = /(?<![\p{L}\d_\\])\[[^[\]]+\](?=_(?:\S|\S.*?\S)_(?![\p{L}\d_]))/gu;
25
+ const InlineAnchorRx = /\[\[([\p{L}_][\p{Alpha}\d_\-:.]*)\]\]/u;
26
+ const InlineImageMacroRx = /image:([^\s:`[\\][^[\\]*)\[(|.*?[^\\])\]/g;
27
+ const InlineStemMacroRx = /stem:\[(.*?[^\\])\]/g;
28
+ const LinkMacroRx = /(\\)?(?:(?:link:(?!:)|(https?:\/\/))([^\s[\\]+)(\[(|.*?[^\\])\^?\])|(https?:\/\/)([^\s[\]]+))/g;
29
+ const ListItemRx = /^(\*+|\.+|<(?:[.1-9]|1\d)>|-|\d+\.) +(.+)/;
30
+ const MarkedSpanRx = /(?<![\p{L}\d_\\])(?:\[((\.line-through)|[^[\]]+)\])?#(\S|\S.*?\S)#(?![\p{L}\d_])/gu;
31
+ const PreprocessorDirectiveRx = /^\\?(?:(if)(n)?def|include)::([^[]+)\[(.+)?\]$/;
32
+ const QuotedSpanRx = /("|')`(\S|\S.*?\S)`\1/g;
33
+ const RevisionInfoLineRx = /^v(\d+(?:[-.]\w+)*)(?:, (\d+-\d+-\d+))?|(\d+-\d+-\d+)$/;
34
+ const RewriteInternalXrefRx = /\[([^[]*?)\]\(#!([^)]+)\)/g;
35
+ const StrongSpanRx = /(?<![\p{L}\d_\\])(?:\[[^[\]]+\])?(\*(?:\S|\S.*?\S)\*)(?![\p{L}\d_])/gu;
36
+ const StyleShorthandMarkersRx = /([.#%])/;
37
+ const XrefMacroRx = /(\\)?xref:(?![\s:`])(?:([^#[\\]+)(#[^\s[\\]*|\.adoc)|#([^\s[\\]+)|([^#[\\]+))\[(|.*?[^\\])\]/g;
38
+ const XrefShorthandRx = /<<([^\s,>][^,>]*)(?:, ?([^>]+))?>>/g;
39
+ /**
40
+ * Detects list type and depth from an AsciiDoc line.
41
+ *
42
+ * @param {string} line - AsciiDoc line to analyze
43
+ * @returns {{ type: 'ordered'|'unordered'|null, depth: number, indent: number }} List info
44
+ */
45
+ function getAsciiDocListInfo(line) {
46
+ const trimmed = line.trim();
47
+ // Ordered list: ., .., ..., etc.
48
+ const orderedMatch = trimmed.match(/^(\.+) /);
49
+ if (orderedMatch) {
50
+ const depth = orderedMatch[1].length;
51
+ return { type: 'ordered', depth, indent: (depth - 1) * 4 };
52
+ }
53
+ // Unordered list: *, **, ***, etc.
54
+ const unorderedMatch = trimmed.match(/^(\*+) /);
55
+ if (unorderedMatch) {
56
+ const depth = unorderedMatch[1].length;
57
+ return { type: 'unordered', depth, indent: (depth - 1) * 4 };
58
+ }
59
+ return { type: null, depth: 0, indent: 0 };
60
+ }
61
+ /**
62
+ * Resolves AsciiDoc include directives by reading and inlining referenced files,
63
+ * or converts them to Nunjucks include syntax for template processing.
64
+ * Handles simple includes only: include::path[] with empty brackets.
65
+ * Skips commented includes and warns about complex includes with attributes.
66
+ *
67
+ * @param {string} content - AsciiDoc content
68
+ * @param {string} basePath - Base directory for resolving relative include paths
69
+ * @param {number} depth - Current recursion depth (for preventing infinite loops)
70
+ * @param {boolean} useNunjucks - If true, convert to Nunjucks syntax instead of inlining (default: true)
71
+ * @returns {string} Content with includes resolved or converted to Nunjucks
72
+ */
73
+ function resolveIncludes(content, basePath, depth = 0, useNunjucks = true) {
74
+ const MAX_DEPTH = 5;
75
+ if (depth > MAX_DEPTH) {
76
+ console.warn(`Warning: Maximum include depth (${MAX_DEPTH}) exceeded`);
77
+ return content;
78
+ }
79
+ // Split content into lines to check for comments
80
+ const lines = content.split('\n');
81
+ let inBlockComment = false;
82
+ const processedLines = [];
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const line = lines[i];
85
+ const trimmedLine = line.trim();
86
+ // Track block comment state
87
+ if (trimmedLine === '////') {
88
+ inBlockComment = !inBlockComment;
89
+ processedLines.push(line);
90
+ continue;
91
+ }
92
+ // Skip if in block comment or line comment
93
+ if (inBlockComment || trimmedLine.startsWith('//')) {
94
+ processedLines.push(line);
95
+ continue;
96
+ }
97
+ // Check for include directive
98
+ const includeMatch = trimmedLine.match(/^include::([^\[]+)\[([^\]]*)\]$/);
99
+ if (includeMatch) {
100
+ const includePath = includeMatch[1];
101
+ const attributes = includeMatch[2];
102
+ // Parse leveloffset from attributes
103
+ const { leveloffset, hasOtherAttributes } = parseLeveloffset(attributes);
104
+ // Warn about other unsupported attributes (tags, lines, etc.)
105
+ if (hasOtherAttributes) {
106
+ console.warn(`Warning: Include directive has unsupported attributes: include::${includePath}[${attributes}]`);
107
+ console.warn(' Only leveloffset is supported. Other attributes (tags, lines) will be ignored.');
108
+ console.warn(' Consider using asciidoctor preprocessing for complex includes.');
109
+ }
110
+ // If using Nunjucks syntax, convert the include to {% include %} format
111
+ if (useNunjucks) {
112
+ // Convert .adoc extension to .md for the include path
113
+ const mdPath = includePath.replace(/\.adoc$/, '.md');
114
+ // Smart indentation: check if snippet contains top-level list items
115
+ let indent = '';
116
+ const fullPath = path.resolve(basePath, includePath);
117
+ if (fs.existsSync(fullPath)) {
118
+ try {
119
+ const snippetContent = fs.readFileSync(fullPath, 'utf8');
120
+ const snippetListInfo = getAsciiDocListInfo(snippetContent);
121
+ // If snippet contains a top-level list item (depth 1), mark it for special handling
122
+ // Otherwise, preserve the original indentation
123
+ if (snippetListInfo.type && snippetListInfo.depth === 1) {
124
+ // Depth-1 list item: will be added without indentation after blank line
125
+ indent = '__NUNJUCKS_TOPLEVEL__';
126
+ }
127
+ else if (!snippetListInfo.type || snippetListInfo.depth > 1) {
128
+ indent = line.match(/^(\s*)/)[1];
129
+ }
130
+ // snippetListInfo.depth === 1 gets special marker
131
+ }
132
+ catch (error) {
133
+ // If we can't read the file, preserve original indentation
134
+ indent = line.match(/^(\s*)/)[1];
135
+ }
136
+ }
137
+ else {
138
+ indent = line.match(/^(\s*)/)[1];
139
+ }
140
+ // Output Nunjucks include syntax with optional leveloffset wrapper
141
+ if (leveloffset) {
142
+ // Minja format with leveloffset
143
+ if (indent === '__NUNJUCKS_TOPLEVEL__') {
144
+ processedLines.push(`__TOPLEVEL__{% leveloffset ${leveloffset} %}` +
145
+ `{% include "./${mdPath}" %}` +
146
+ `{% endleveloffset %}`);
147
+ }
148
+ else {
149
+ processedLines.push(`${indent}{% leveloffset ${leveloffset} %}` +
150
+ `{% include "./${mdPath}" %}` +
151
+ `{% endleveloffset %}`);
152
+ }
153
+ }
154
+ else {
155
+ // Standard Nunjucks include (existing logic)
156
+ if (indent === '__NUNJUCKS_TOPLEVEL__') {
157
+ // Top-level list includes: use special prefix to prevent main converter from indenting
158
+ // The __TOPLEVEL__ prefix will be removed in post-processing
159
+ processedLines.push(`__TOPLEVEL__{% include "./${mdPath}" %}`);
160
+ }
161
+ else {
162
+ processedLines.push(`${indent}{% include "./${mdPath}" %}`);
163
+ }
164
+ }
165
+ continue;
166
+ }
167
+ // Otherwise, inline the content (legacy behavior)
168
+ const fullPath = path.resolve(basePath, includePath);
169
+ // Check if file exists
170
+ if (!fs.existsSync(fullPath)) {
171
+ console.warn(`Warning: Include file not found: ${includePath}`);
172
+ processedLines.push(`<!-- WARNING: Include not found: ${includePath} -->`);
173
+ continue;
174
+ }
175
+ // Read and process the included file
176
+ try {
177
+ const includeContent = fs.readFileSync(fullPath, 'utf8');
178
+ const includeBasePath = path.dirname(fullPath);
179
+ // Recursively resolve includes in the included file
180
+ const resolvedContent = resolveIncludes(includeContent, includeBasePath, depth + 1, useNunjucks);
181
+ // Add the resolved content (preserving the indentation of the include directive)
182
+ const indent = line.match(/^(\s*)/)[1];
183
+ const indentedContent = resolvedContent.split('\n').map((l, idx) => idx === 0 ? l : (l ? indent + l : l)).join('\n');
184
+ processedLines.push(indentedContent);
185
+ }
186
+ catch (error) {
187
+ console.warn(`Warning: Error reading include file ${includePath}: ${error.message}`);
188
+ processedLines.push(`<!-- WARNING: Error reading include: ${includePath} -->`);
189
+ }
190
+ }
191
+ else {
192
+ processedLines.push(line);
193
+ }
194
+ }
195
+ return processedLines.join('\n');
196
+ }
197
+ /**
198
+ * Parses the leveloffset attribute from an include directive's attribute string.
199
+ * Also detects if other unsupported attributes are present.
200
+ *
201
+ * @param {string} attributeString - The content inside brackets of include directive
202
+ * @returns {object} { leveloffset: string|null, hasOtherAttributes: boolean }
203
+ */
204
+ function parseLeveloffset(attributeString) {
205
+ if (!attributeString || !attributeString.trim()) {
206
+ return { leveloffset: null, hasOtherAttributes: false };
207
+ }
208
+ // Split by comma to handle multiple attributes
209
+ const attrs = attributeString.split(',').map(a => a.trim());
210
+ let leveloffset = null;
211
+ let hasOtherAttributes = false;
212
+ for (const attr of attrs) {
213
+ if (attr.startsWith('leveloffset=')) {
214
+ leveloffset = attr.substring('leveloffset='.length).trim();
215
+ }
216
+ else if (attr) {
217
+ hasOtherAttributes = true;
218
+ }
219
+ }
220
+ return { leveloffset, hasOtherAttributes };
221
+ }
222
+ /**
223
+ * Converts AsciiDoc attribute assignments to Jinja2 set statements.
224
+ *
225
+ * @param {string} line - Line that may contain attribute assignment
226
+ * @param {boolean} stripRight - Whether to use right-strip (-%}) or not (%})
227
+ * @returns {string|null} - Jinja2 set statement or null if not an attribute assignment
228
+ */
229
+ function convertAttributeAssignment(line, stripRight = true) {
230
+ const trimmed = line.trim();
231
+ const rightStrip = stripRight ? '-' : '';
232
+ // Match AsciiDoc attribute unset: :!name:
233
+ const unsetMatch = trimmed.match(/^:!([a-zA-Z0-9_-]+):$/);
234
+ if (unsetMatch) {
235
+ const [, name] = unsetMatch;
236
+ const varName = name.replace(/-/g, '_');
237
+ return `{%- set ${varName} = false ${rightStrip}%}`;
238
+ }
239
+ // Match AsciiDoc attribute assignment: :name: or :name: value
240
+ // Format: :attr-name: or :attr-name: value
241
+ const match = trimmed.match(/^:([a-zA-Z0-9_-]+):(?:\s+(.+))?$/);
242
+ if (!match) {
243
+ return null;
244
+ }
245
+ const [, name, value] = match;
246
+ // Convert attribute name to valid Jinja2 variable name
247
+ // Replace hyphens with underscores
248
+ const varName = name.replace(/-/g, '_');
249
+ if (value && value.trim()) {
250
+ // Attribute with value: :name: value
251
+ // Convert to: {% set name = "value" %}
252
+ let processedValue = value.trim();
253
+ // Expand attribute references in value: {attr} → {{ attr }}
254
+ processedValue = processedValue.replace(/\{([a-zA-Z0-9_-]+)\}/g, (_, attr) => {
255
+ return '{{ ' + attr.replace(/-/g, '_') + ' }}';
256
+ });
257
+ // Convert link macros: link:URL[text] → [text](URL)
258
+ // URL may contain {{ attr }} template variables (with spaces), so match those explicitly
259
+ processedValue = processedValue.replace(/link:((?:\{\{[^}]+\}\}|[^\s\[])+)\[([^\]]*)\]/g, '[$2]($1)');
260
+ // Expand UI macros in attribute values
261
+ // menu:Name[Item] → **Name → Item**
262
+ processedValue = processedValue.replace(/menu:([^\[]+)\[([^\]]+)\]/g, (_, menu, path) => {
263
+ const parts = path.split(' > ').map(s => s.trim());
264
+ let result = menu;
265
+ parts.forEach((part) => {
266
+ result += ' → ' + part;
267
+ });
268
+ return '**' + result + '**';
269
+ });
270
+ // btn:[text] → **[text]**
271
+ processedValue = processedValue.replace(/btn:\[([^\]]+)\]/g, '**[$1]**');
272
+ const quotedValue = `"${processedValue}"`;
273
+ return `{%- set ${varName} = ${quotedValue} ${rightStrip}%}`;
274
+ }
275
+ else {
276
+ // Boolean attribute: :name:
277
+ // Convert to: {% set name = true %}
278
+ return `{%- set ${varName} = true ${rightStrip}%}`;
279
+ }
280
+ }
281
+ /**
282
+ * Checks if a line is a template directive (conditional or attribute assignment).
283
+ * @param {string} line - Line to check
284
+ * @returns {boolean} True if line is a template directive
285
+ */
286
+ function isTemplateDirective(line) {
287
+ const trimmed = line.trim();
288
+ if (!trimmed)
289
+ return false;
290
+ // Check for conditional directives
291
+ if (trimmed.match(/^ifdef::/))
292
+ return true;
293
+ if (trimmed.match(/^ifndef::/))
294
+ return true;
295
+ if (trimmed.match(/^ifeval::/))
296
+ return true;
297
+ if (trimmed.match(/^endif::/))
298
+ return true;
299
+ // Check for attribute assignment or unset
300
+ if (trimmed.match(/^:!?[a-zA-Z0-9_-]+:/))
301
+ return true;
302
+ return false;
303
+ }
304
+ /**
305
+ * Checks if a line has non-whitespace content.
306
+ * @param {string} line - Line to check
307
+ * @returns {boolean} True if line has content
308
+ */
309
+ function hasContentOnLine(line) {
310
+ return line && line.trim().length > 0;
311
+ }
312
+ /**
313
+ * Detects if opening tag should be inline (previous line has non-template content).
314
+ * @param {Array<string>} lines - All lines
315
+ * @param {number} currentIndex - Current line index
316
+ * @returns {boolean} True if should use inline (left-strip) for opening tag
317
+ */
318
+ function isInlineOpeningContext(lines, currentIndex) {
319
+ if (currentIndex === 0)
320
+ return false;
321
+ const prevLine = lines[currentIndex - 1];
322
+ // Only inline if previous line has content AND is not a template directive
323
+ // This excludes nested conditionals and attribute assignments
324
+ return hasContentOnLine(prevLine) && !isTemplateDirective(prevLine);
325
+ }
326
+ /**
327
+ * Detects if closing tag should be inline (next line has non-template content).
328
+ * @param {Array<string>} lines - All lines
329
+ * @param {number} currentIndex - Current line index
330
+ * @returns {boolean} True if should use inline (left-strip) for closing tag
331
+ */
332
+ function isInlineClosingContext(lines, currentIndex) {
333
+ // Only inline if next line exists and has content that's not a template directive
334
+ if (currentIndex + 1 < lines.length) {
335
+ const nextLine = lines[currentIndex + 1];
336
+ // Only inline if next line has content AND is not a template directive
337
+ // This excludes nested conditionals and attribute assignments
338
+ return hasContentOnLine(nextLine) && !isTemplateDirective(nextLine);
339
+ }
340
+ return false;
341
+ }
342
+ /**
343
+ * Checks if a table contains conditionals or other complex features requiring HTML output.
344
+ * Complex features include: conditionals (ifdef, ifndef, ifeval), colspan, rowspan.
345
+ *
346
+ * @param {Array<string>} tableLines - Lines within the table (between |=== markers)
347
+ * @returns {boolean} True if table needs HTML conversion
348
+ */
349
+ function tableNeedsHtmlConversion(tableLines) {
350
+ for (const line of tableLines) {
351
+ const trimmed = line.trim();
352
+ // Check for conditionals
353
+ if (trimmed.match(/^ifdef::|^ifndef::|^ifeval::|^endif::/)) {
354
+ return true;
355
+ }
356
+ // Check for inline conditionals: ifdef::attr[content] or ifndef::attr[content]
357
+ if (trimmed.match(/ifdef::[^\[]+\[.+\]/) || trimmed.match(/ifndef::[^\[]+\[.+\]/)) {
358
+ return true;
359
+ }
360
+ // Check for AsciiDoc cells (a|) which can contain complex content like lists
361
+ if (trimmed.match(/^a\|/) || trimmed.match(/\|?\s*a\|/)) {
362
+ return true;
363
+ }
364
+ // Check for colspan (N+|) patterns
365
+ if (/^\d+\+\|/.test(trimmed)) {
366
+ return true;
367
+ }
368
+ }
369
+ return false;
370
+ }
371
+ /**
372
+ * Converts an AsciiDoc conditional to Jinja2 format for inline use in HTML.
373
+ * @param {string} directive - The conditional directive (ifdef, ifndef, etc.)
374
+ * @param {string} attrs - The attribute(s) being tested
375
+ * @returns {string} Jinja2 conditional string
376
+ */
377
+ function conditionalToJinja2(directive, attrs) {
378
+ // Parse attributes with both OR (,) and AND (+) operators
379
+ const orGroups = attrs.split(',').map(group => {
380
+ const andAttrs = group.trim().split('+').map(a => a.trim().replace(/-/g, '_'));
381
+ return andAttrs.length === 1 ? andAttrs[0] : `(${andAttrs.join(' and ')})`;
382
+ });
383
+ if (directive === 'ifdef') {
384
+ const condition = orGroups.join(' or ');
385
+ return `{% if ${condition} %}`;
386
+ }
387
+ else if (directive === 'ifndef') {
388
+ if (orGroups.length === 1 && !orGroups[0].includes('(')) {
389
+ return `{% if not ${orGroups[0]} %}`;
390
+ }
391
+ else {
392
+ const condition = orGroups.join(' or ');
393
+ return `{% if not (${condition}) %}`;
394
+ }
395
+ }
396
+ return '';
397
+ }
398
+ /**
399
+ * Parses AsciiDoc table column spec to determine column count and alignments.
400
+ * @param {string} colSpec - Column specification (e.g., "2,2,2,2,2,2" or "3*")
401
+ * @returns {Object} { count: number, aligns: Array<string> }
402
+ */
403
+ function parseTableColSpec(colSpec) {
404
+ if (!colSpec)
405
+ return { count: 0, aligns: [] };
406
+ // In AsciiDoc, a bare number (e.g., cols="3") means 3 equal-width columns
407
+ if (/^\d+$/.test(colSpec.trim())) {
408
+ const count = parseInt(colSpec.trim(), 10);
409
+ return { count, aligns: new Array(count).fill('') };
410
+ }
411
+ const cols = [];
412
+ const parts = colSpec.split(/,|;/);
413
+ for (const part of parts) {
414
+ const multiplierMatch = part.match(/^(\d+)\*(.*)$/);
415
+ if (multiplierMatch) {
416
+ const count = parseInt(multiplierMatch[1], 10);
417
+ const align = multiplierMatch[2] || '';
418
+ for (let i = 0; i < count; i++)
419
+ cols.push(align);
420
+ }
421
+ else {
422
+ cols.push(part);
423
+ }
424
+ }
425
+ // Determine alignment from column spec
426
+ const aligns = cols.map(c => {
427
+ if (c.includes('<'))
428
+ return 'left';
429
+ if (c.includes('>'))
430
+ return 'right';
431
+ if (c.includes('^'))
432
+ return 'center';
433
+ return '';
434
+ });
435
+ return { count: cols.length, aligns };
436
+ }
437
+ /**
438
+ * Converts an AsciiDoc table with conditionals to HTML format.
439
+ * This preserves conditionals inline within the HTML structure.
440
+ *
441
+ * @param {Array<string>} tableLines - Lines within the table (between |=== markers)
442
+ * @param {Object} options - Table options from block attributes
443
+ * @returns {string} HTML table with inline Jinja2 conditionals
444
+ */
445
+ // Marker for HTML content that should pass through without processing
446
+ const HTML_PASSTHROUGH_MARKER = '__HTML_PASSTHROUGH__';
447
+ function convertTableToHtml(tableLines, options = {}) {
448
+ const { colSpec, hasHeader } = options;
449
+ const { count: specColCount, aligns } = parseTableColSpec(colSpec);
450
+ const result = [];
451
+ const M = HTML_PASSTHROUGH_MARKER; // Shorthand
452
+ result.push(M + '<table>');
453
+ let headerWritten = false;
454
+ let bodyStarted = false;
455
+ let currentRow = [];
456
+ let conditionalStack = [];
457
+ let isFirstRow = true;
458
+ // Column count: from colSpec, or inferred from first row
459
+ let columnCount = specColCount || 0;
460
+ // Helper to output current row
461
+ function outputRow() {
462
+ if (currentRow.length === 0)
463
+ return;
464
+ // Set column count from first row if not known
465
+ if (columnCount === 0)
466
+ columnCount = currentRow.length;
467
+ const isHeader = isFirstRow && (hasHeader !== false);
468
+ const tag = isHeader ? 'th' : 'td';
469
+ function renderCells(tag) {
470
+ for (const cell of currentRow) {
471
+ const colspanAttr = cell.colspan ? ` colspan="${cell.colspan}"` : '';
472
+ if (cell.cellWrapper) {
473
+ result.push(M + ` ${cell.cellWrapper.open}<${tag}${colspanAttr}>${cell.content}</${tag}>${cell.cellWrapper.close}`);
474
+ }
475
+ else {
476
+ result.push(M + ` <${tag}${colspanAttr}>${cell.content}</${tag}>`);
477
+ }
478
+ }
479
+ }
480
+ if (isHeader && !headerWritten) {
481
+ result.push(M + '<thead>');
482
+ result.push(M + '<tr>');
483
+ renderCells(tag);
484
+ result.push(M + '</tr>');
485
+ result.push(M + '</thead>');
486
+ headerWritten = true;
487
+ }
488
+ else {
489
+ if (!bodyStarted) {
490
+ result.push(M + '<tbody>');
491
+ bodyStarted = true;
492
+ }
493
+ result.push(M + '<tr>');
494
+ renderCells(tag);
495
+ result.push(M + '</tr>');
496
+ }
497
+ currentRow = [];
498
+ isFirstRow = false;
499
+ }
500
+ // Count effective columns, treating consecutive conditional cells as one column
501
+ function effectiveColumnCount() {
502
+ let count = 0;
503
+ let prevConditionalWrapper = null;
504
+ for (const cell of currentRow) {
505
+ if (cell.colspan) {
506
+ count += cell.colspan;
507
+ prevConditionalWrapper = null;
508
+ }
509
+ else if (cell.cellWrapper) {
510
+ // Same wrapper as previous = cells from one conditional block → count each
511
+ // Different wrapper from previous = alternatives for same column → count as one
512
+ if (prevConditionalWrapper === null || cell.cellWrapper.open === prevConditionalWrapper) {
513
+ count++;
514
+ }
515
+ prevConditionalWrapper = cell.cellWrapper.open;
516
+ }
517
+ else {
518
+ count++;
519
+ prevConditionalWrapper = null;
520
+ }
521
+ }
522
+ return count;
523
+ }
524
+ // Check if current row is complete (has enough cells) and output if so
525
+ function checkRowComplete() {
526
+ if (columnCount > 0 && effectiveColumnCount() >= columnCount) {
527
+ outputRow();
528
+ }
529
+ }
530
+ // Helper: convert inline AsciiDoc markup in a string (for use inside HTML)
531
+ function convertInlineMarkup(text) {
532
+ let s = text;
533
+ // Convert attribute references
534
+ s = s.replace(/\{([a-zA-Z0-9_-]+)\}/g, (_, attr) => '{{ ' + attr.replace(/-/g, '_') + ' }}');
535
+ // Convert link macros: link:URL[text] → <a href="URL">text</a>
536
+ // URL may contain {{ attr }} template variables (with spaces) or regular URLs
537
+ s = s.replace(/link:((?:\{\{[^}]+\}\}|[^\s\[])+)\[([^\]]*)\]/g, '<a href="$1">$2</a>');
538
+ // Convert xref macros: xref:target[text] → <a href="#target">text</a>
539
+ s = s.replace(/xref:([^\[]*)\[([^\]]*)\]/g, '<a href="#$1">$2</a>');
540
+ // Convert monospace: `text` → <code>text</code> (also handle `+text+` passthrough)
541
+ s = s.replace(/`\+?([^`]+?)\+?`/g, '<code>$1</code>');
542
+ // Convert bold: *text* → <strong>text</strong>
543
+ s = s.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<strong>$1</strong>');
544
+ // Convert italic: _text_ → <em>text</em> (only at word boundaries)
545
+ s = s.replace(/(?<!\w)_([^_]+)_(?!\w)/g, '<em>$1</em>');
546
+ return s;
547
+ }
548
+ // Helper: escape HTML special characters for code blocks
549
+ function escapeHtml(text) {
550
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
551
+ }
552
+ // Helper: convert AsciiDoc cell content lines to HTML
553
+ function convertCellContent(lines) {
554
+ let html = '';
555
+ let listType = null; // 'ul' or 'ol'
556
+ let inCodeBlock = false;
557
+ let codeLines = [];
558
+ let pendingAdmonition = null; // 'NOTE', 'WARNING', etc.
559
+ let inAdmonition = null;
560
+ let admonitionLines = [];
561
+ function closeList() {
562
+ if (listType) {
563
+ html += '</' + listType + '>';
564
+ listType = null;
565
+ }
566
+ }
567
+ function flushCodeBlock() {
568
+ if (codeLines.length > 0 || inCodeBlock) {
569
+ closeList();
570
+ html += '<pre>' + escapeHtml(codeLines.join('\n')).replace(/\n/g, '&#10;') + '</pre>';
571
+ codeLines = [];
572
+ inCodeBlock = false;
573
+ }
574
+ }
575
+ function flushAdmonition() {
576
+ if (inAdmonition) {
577
+ closeList();
578
+ const admonType = inAdmonition.charAt(0) + inAdmonition.slice(1).toLowerCase();
579
+ const content = admonitionLines.map(l => l === '' ? '<br><br>' : convertInlineMarkup(l)).join('');
580
+ html += '<dl><dt>' + admonType + '</dt><dd>' + content + '</dd></dl>';
581
+ inAdmonition = null;
582
+ admonitionLines = [];
583
+ }
584
+ }
585
+ for (const l of lines) {
586
+ const t = l.trim();
587
+ // Code block delimiter (----)
588
+ if (/^-{4,}\s*$/.test(t)) {
589
+ if (inCodeBlock) {
590
+ flushCodeBlock();
591
+ }
592
+ else {
593
+ closeList();
594
+ inCodeBlock = true;
595
+ codeLines = [];
596
+ }
597
+ continue;
598
+ }
599
+ // Code block content: accumulate verbatim
600
+ if (inCodeBlock) {
601
+ codeLines.push(t);
602
+ continue;
603
+ }
604
+ // Admonition header: [NOTE], [WARNING], etc.
605
+ const admonMatch = t.match(/^\[(NOTE|IMPORTANT|WARNING|CAUTION|TIP)\]\s*$/);
606
+ if (admonMatch) {
607
+ closeList();
608
+ pendingAdmonition = admonMatch[1];
609
+ continue;
610
+ }
611
+ // Admonition delimiter (====): opens or closes admonition block
612
+ if (/^={4,}\s*$/.test(t)) {
613
+ if (pendingAdmonition) {
614
+ inAdmonition = pendingAdmonition;
615
+ pendingAdmonition = null;
616
+ admonitionLines = [];
617
+ continue;
618
+ }
619
+ else if (inAdmonition) {
620
+ flushAdmonition();
621
+ continue;
622
+ }
623
+ // Standalone ==== without admonition context: treat as regular text
624
+ }
625
+ // If [NOTE] was seen but next line wasn't ====, emit [NOTE] as text
626
+ if (pendingAdmonition) {
627
+ html += convertInlineMarkup('[' + pendingAdmonition + ']');
628
+ pendingAdmonition = null;
629
+ // Fall through to process current line normally
630
+ }
631
+ // Admonition content: accumulate
632
+ if (inAdmonition) {
633
+ admonitionLines.push(t);
634
+ continue;
635
+ }
636
+ // Block attributes ([literal, ...], [source,yaml], etc.): strip
637
+ if (/^\[.*\]\s*$/.test(t)) {
638
+ continue;
639
+ }
640
+ // Blank line: paragraph break
641
+ if (t === '') {
642
+ closeList();
643
+ if (html)
644
+ html += '<br><br>';
645
+ continue;
646
+ }
647
+ // Unordered list item (* item)
648
+ if (t.startsWith('* ')) {
649
+ if (listType !== 'ul') {
650
+ closeList();
651
+ html += '<ul>';
652
+ listType = 'ul';
653
+ }
654
+ html += '<li>' + convertInlineMarkup(t.substring(2)) + '</li>';
655
+ continue;
656
+ }
657
+ // Unordered list item (- item)
658
+ if (t.startsWith('- ')) {
659
+ if (listType !== 'ul') {
660
+ closeList();
661
+ html += '<ul>';
662
+ listType = 'ul';
663
+ }
664
+ html += '<li>' + convertInlineMarkup(t.substring(2)) + '</li>';
665
+ continue;
666
+ }
667
+ // Ordered list item (. item)
668
+ if (t.startsWith('. ') && !t.startsWith('.. ')) {
669
+ if (listType !== 'ol') {
670
+ closeList();
671
+ html += '<ol>';
672
+ listType = 'ol';
673
+ }
674
+ html += '<li>' + convertInlineMarkup(t.substring(2)) + '</li>';
675
+ continue;
676
+ }
677
+ // Regular text
678
+ closeList();
679
+ html += convertInlineMarkup(t);
680
+ }
681
+ // Flush any unclosed blocks
682
+ if (inCodeBlock)
683
+ flushCodeBlock();
684
+ if (inAdmonition)
685
+ flushAdmonition();
686
+ closeList();
687
+ // Trim trailing <br> breaks from blank lines at end of cell
688
+ return html.replace(/(<br>)+$/, '');
689
+ }
690
+ // Helper: apply attribute reference conversion and inline conditionals to a cell string
691
+ function convertCellString(str) {
692
+ let s = str;
693
+ // Handle inline conditionals with pipe
694
+ s = s.replace(/ifdef::([^\[]+)\[\|((?:[^\]]|\]\^)*)\]/g, (_, attrs, content) => {
695
+ return '|__COND_CELL__' + conditionalToJinja2('ifdef', attrs) + '__COND_SEP__' + content + '__COND_END__';
696
+ });
697
+ s = s.replace(/ifndef::([^\[]+)\[\|((?:[^\]]|\]\^)*)\]/g, (_, attrs, content) => {
698
+ return '|__COND_CELL__' + conditionalToJinja2('ifndef', attrs) + '__COND_SEP__' + content + '__COND_END__';
699
+ });
700
+ // Handle inline conditionals without pipe
701
+ s = s.replace(/ifdef::([^\[]+)\[((?:[^\]]|\]\^)*)\]/g, (_, attrs, content) => {
702
+ return conditionalToJinja2('ifdef', attrs) + content + '{% endif %}';
703
+ });
704
+ s = s.replace(/ifndef::([^\[]+)\[((?:[^\]]|\]\^)*)\]/g, (_, attrs, content) => {
705
+ return conditionalToJinja2('ifndef', attrs) + content + '{% endif %}';
706
+ });
707
+ // Convert inline markup (attribute refs, links, xrefs) for HTML context
708
+ s = convertInlineMarkup(s);
709
+ return s;
710
+ }
711
+ // Helper: add a cell to currentRow with appropriate wrappers
712
+ function addCell(content, stackWrapper) {
713
+ const condCellMatch = content.match(/^__COND_CELL__(.+?)__COND_SEP__(.*)__COND_END__$/);
714
+ if (condCellMatch) {
715
+ const [, condOpen, innerContent] = condCellMatch;
716
+ if (stackWrapper) {
717
+ currentRow.push({ content: innerContent, cellWrapper: { open: stackWrapper.open + condOpen, close: '{% endif %}' + stackWrapper.close } });
718
+ }
719
+ else {
720
+ currentRow.push({ content: innerContent, cellWrapper: { open: condOpen, close: '{% endif %}' } });
721
+ }
722
+ }
723
+ else if (stackWrapper) {
724
+ currentRow.push({ content, cellWrapper: stackWrapper });
725
+ }
726
+ else {
727
+ currentRow.push({ content, cellWrapper: null });
728
+ }
729
+ }
730
+ let inAsciidocCell = false;
731
+ let asciidocCellLines = [];
732
+ let asciidocCellWrapper = null;
733
+ // Helper: flush collected a| cell content
734
+ function flushAsciidocCell() {
735
+ if (!inAsciidocCell)
736
+ return;
737
+ const content = convertCellContent(asciidocCellLines);
738
+ addCell(content, asciidocCellWrapper);
739
+ inAsciidocCell = false;
740
+ asciidocCellLines = [];
741
+ asciidocCellWrapper = null;
742
+ }
743
+ for (let i = 0; i < tableLines.length; i++) {
744
+ const line = tableLines[i];
745
+ const trimmed = line.trim();
746
+ // Empty line handling
747
+ if (!trimmed) {
748
+ if (inAsciidocCell) {
749
+ // Blank lines within a| cells are part of the cell content (paragraph breaks)
750
+ asciidocCellLines.push('');
751
+ continue;
752
+ }
753
+ checkRowComplete();
754
+ // If column count is unknown (first row not yet complete), use empty line as row boundary
755
+ if (columnCount === 0 && currentRow.length > 0) {
756
+ outputRow();
757
+ }
758
+ continue;
759
+ }
760
+ // Skip AsciiDoc comments (lines starting with //)
761
+ // Special case: when in a| cell, a comment ending with "| value" may contain
762
+ // a cell delimiter the author intended to preserve (common authoring mistake)
763
+ if (trimmed.startsWith('//')) {
764
+ if (inAsciidocCell) {
765
+ const commentText = trimmed.substring(2);
766
+ // Find last | in the comment that has non-empty text before it (not "// | cell | cell" patterns)
767
+ const lastPipeIdx = commentText.lastIndexOf('|');
768
+ if (lastPipeIdx > 0) {
769
+ const beforePipe = commentText.substring(0, lastPipeIdx).trim();
770
+ const afterPipe = commentText.substring(lastPipeIdx + 1).trim();
771
+ // Only extract if: text before | is substantial, text after | is a short value,
772
+ // and there's no other | before (which would indicate a full commented row)
773
+ if (beforePipe.length > 0 && afterPipe.length > 0 && afterPipe.length <= 20
774
+ && !beforePipe.includes('|')) {
775
+ // End the a| cell and add the extracted cell value
776
+ flushAsciidocCell();
777
+ checkRowComplete();
778
+ const sw = conditionalStack.length > 0
779
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
780
+ : null;
781
+ addCell(convertCellString(afterPipe), sw);
782
+ checkRowComplete();
783
+ continue;
784
+ }
785
+ }
786
+ }
787
+ continue;
788
+ }
789
+ // Handle block conditionals (full line ifdef/ifndef/endif)
790
+ const blockIfdefMatch = trimmed.match(/^ifdef::([^\[]+)\[\]$/);
791
+ if (blockIfdefMatch) {
792
+ const jinja = conditionalToJinja2('ifdef', blockIfdefMatch[1]);
793
+ conditionalStack.push(jinja);
794
+ continue;
795
+ }
796
+ const blockIfndefMatch = trimmed.match(/^ifndef::([^\[]+)\[\]$/);
797
+ if (blockIfndefMatch) {
798
+ const jinja = conditionalToJinja2('ifndef', blockIfndefMatch[1]);
799
+ conditionalStack.push(jinja);
800
+ continue;
801
+ }
802
+ if (trimmed.match(/^endif::/)) {
803
+ if (conditionalStack.length > 0) {
804
+ conditionalStack.pop();
805
+ }
806
+ continue;
807
+ }
808
+ // If collecting a| cell content, check if this line starts a new cell or continues content
809
+ if (inAsciidocCell) {
810
+ if (trimmed.startsWith('|') || trimmed.startsWith('a|')) {
811
+ flushAsciidocCell();
812
+ checkRowComplete();
813
+ // Fall through to handle the new cell below
814
+ }
815
+ else {
816
+ // Check for inline a| cell boundary (e.g., "---- a| ----")
817
+ const inlineAIdx = trimmed.indexOf(' a|');
818
+ if (inlineAIdx >= 0) {
819
+ // Content before ' a|' belongs to current cell
820
+ const beforeContent = trimmed.substring(0, inlineAIdx).trim();
821
+ if (beforeContent) {
822
+ asciidocCellLines.push(beforeContent);
823
+ }
824
+ flushAsciidocCell();
825
+ checkRowComplete();
826
+ // Start new a| cell
827
+ const stackWrapper = conditionalStack.length > 0
828
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
829
+ : null;
830
+ inAsciidocCell = true;
831
+ asciidocCellLines = [];
832
+ asciidocCellWrapper = stackWrapper;
833
+ const afterContent = trimmed.substring(inlineAIdx + 3).trim();
834
+ if (afterContent) {
835
+ asciidocCellLines.push(afterContent);
836
+ }
837
+ continue;
838
+ }
839
+ // Check for inline | cell boundary (e.g., "...content.| Yes")
840
+ // Find last | that is NOT inside [] brackets
841
+ let pipeIdx = -1;
842
+ let bracketDepth = 0;
843
+ for (let c = trimmed.length - 1; c >= 0; c--) {
844
+ if (trimmed[c] === ']')
845
+ bracketDepth++;
846
+ else if (trimmed[c] === '[')
847
+ bracketDepth--;
848
+ else if (trimmed[c] === '|' && bracketDepth === 0) {
849
+ pipeIdx = c;
850
+ break;
851
+ }
852
+ }
853
+ if (pipeIdx > 0) {
854
+ // Content before | belongs to current cell
855
+ const beforeContent = trimmed.substring(0, pipeIdx).trim();
856
+ if (beforeContent) {
857
+ asciidocCellLines.push(beforeContent);
858
+ }
859
+ flushAsciidocCell();
860
+ checkRowComplete();
861
+ // Re-process the | and everything after it as a new cell line
862
+ const cellPart = trimmed.substring(pipeIdx);
863
+ // Fall through to the cell parsing below by updating trimmed/line
864
+ // We can't easily re-enter the loop, so handle it directly
865
+ const afterPipe = cellPart.substring(1).trim();
866
+ const stackWrapper = conditionalStack.length > 0
867
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
868
+ : null;
869
+ if (afterPipe === '') {
870
+ addCell('', stackWrapper);
871
+ }
872
+ else {
873
+ const converted = convertCellString(afterPipe);
874
+ addCell(converted, stackWrapper);
875
+ }
876
+ checkRowComplete();
877
+ continue;
878
+ }
879
+ // Continuation of a| cell content (bullet lists, text, etc.)
880
+ asciidocCellLines.push(trimmed);
881
+ continue;
882
+ }
883
+ }
884
+ // Handle inline conditional cells (e.g., ifndef::ibm-z[|300])
885
+ // The | inside the brackets is a real AsciiDoc cell delimiter - the conditional
886
+ // provides a cell value. We parse the whole construct here to avoid the naive
887
+ // | split below from breaking it into separate parts.
888
+ const inlineCondCellMatch = trimmed.match(/^(ifdef|ifndef)::([^\[]+)\[\|(.*)\]$/);
889
+ if (inlineCondCellMatch) {
890
+ const [, directive, attrs, content] = inlineCondCellMatch;
891
+ const jinja = conditionalToJinja2(directive, attrs);
892
+ const converted = content.replace(/\{([a-zA-Z0-9_-]+)\}/g, (_, attr) => '{{ ' + attr.replace(/-/g, '_') + ' }}');
893
+ currentRow.push({ content: converted, cellWrapper: { open: jinja, close: '{% endif %}' } });
894
+ checkRowComplete();
895
+ continue;
896
+ }
897
+ // Handle colspan: N+| content → cell spanning N columns
898
+ const colspanMatch = trimmed.match(/^(\d+)\+\|\s*(.*)$/);
899
+ if (colspanMatch) {
900
+ const colspan = parseInt(colspanMatch[1], 10);
901
+ const content = convertCellString(colspanMatch[2]);
902
+ const stackWrapper = conditionalStack.length > 0
903
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
904
+ : null;
905
+ currentRow.push({ content, cellWrapper: stackWrapper, colspan });
906
+ // A colspan cell hints at the total column count (e.g., 2+| in a 2-column table)
907
+ if (columnCount === 0)
908
+ columnCount = colspan;
909
+ checkRowComplete();
910
+ continue;
911
+ }
912
+ // Handle continuation lines (don't start with | or a|)
913
+ // In AsciiDoc tables, text before the first | on such lines continues the previous cell
914
+ if (!trimmed.startsWith('|') && !trimmed.startsWith('a|')) {
915
+ // Check for inline a| in continuation (e.g., "...text. a| - item" or "...text. a|")
916
+ const contAIdx = trimmed.indexOf(' a|');
917
+ if (contAIdx >= 0) {
918
+ const continuation = trimmed.substring(0, contAIdx).trim();
919
+ if (continuation && currentRow.length > 0) {
920
+ currentRow[currentRow.length - 1].content += ' ' + convertCellString(continuation);
921
+ }
922
+ else if (continuation) {
923
+ const sw = conditionalStack.length > 0
924
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
925
+ : null;
926
+ addCell(convertCellString(continuation), sw);
927
+ checkRowComplete();
928
+ }
929
+ // Start a| cell collection
930
+ const stackWrapper = conditionalStack.length > 0
931
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
932
+ : null;
933
+ inAsciidocCell = true;
934
+ asciidocCellLines = [];
935
+ asciidocCellWrapper = stackWrapper;
936
+ const afterContent = trimmed.substring(contAIdx + 3).trim();
937
+ if (afterContent) {
938
+ asciidocCellLines.push(afterContent);
939
+ }
940
+ continue;
941
+ }
942
+ if (trimmed.includes('|')) {
943
+ // Line has | mid-line: first part continues previous cell, rest are new cells
944
+ const firstPipeIdx = trimmed.indexOf('|');
945
+ const continuation = trimmed.substring(0, firstPipeIdx).trim();
946
+ if (continuation && currentRow.length > 0) {
947
+ currentRow[currentRow.length - 1].content += ' ' + convertCellString(continuation);
948
+ }
949
+ else if (continuation) {
950
+ // No previous cell to append to - treat as new cell
951
+ const stackWrapper = conditionalStack.length > 0
952
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
953
+ : null;
954
+ addCell(convertCellString(continuation), stackWrapper);
955
+ checkRowComplete();
956
+ }
957
+ // Process rest of line (starting from |) as new cells by re-entering the cell parser
958
+ const remainingCells = trimmed.substring(firstPipeIdx);
959
+ let remContent = remainingCells.substring(1); // Remove leading |
960
+ if (remContent.trim() === '') {
961
+ const sw = conditionalStack.length > 0
962
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
963
+ : null;
964
+ addCell('', sw);
965
+ checkRowComplete();
966
+ }
967
+ else {
968
+ const remParts = remContent.split('|');
969
+ const sw = conditionalStack.length > 0
970
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
971
+ : null;
972
+ for (const rp of remParts) {
973
+ const rpTrimmed = rp.trim();
974
+ if (rpTrimmed === '')
975
+ continue;
976
+ addCell(convertCellString(rpTrimmed), sw);
977
+ checkRowComplete();
978
+ }
979
+ }
980
+ continue;
981
+ }
982
+ else {
983
+ // No | at all - pure continuation of previous cell
984
+ if (currentRow.length > 0) {
985
+ currentRow[currentRow.length - 1].content += ' ' + convertCellString(trimmed);
986
+ }
987
+ continue;
988
+ }
989
+ }
990
+ // Handle table cells (lines starting with |, a|, or containing |)
991
+ if (trimmed.startsWith('|') || trimmed.startsWith('a|') || trimmed.includes('|')) {
992
+ // Parse cells from the line
993
+ let cellContent = trimmed;
994
+ let cellsAddedThisLine = 0;
995
+ // Remove leading | if present
996
+ if (cellContent.startsWith('|')) {
997
+ cellContent = cellContent.substring(1);
998
+ }
999
+ // Handle empty cell (line is just "|" or "| ")
1000
+ if (cellContent.trim() === '') {
1001
+ const stackWrapper = conditionalStack.length > 0
1002
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
1003
+ : null;
1004
+ addCell('', stackWrapper);
1005
+ checkRowComplete();
1006
+ continue;
1007
+ }
1008
+ const parts = cellContent.split('|');
1009
+ const stackWrapper = conditionalStack.length > 0
1010
+ ? { open: conditionalStack.join(''), close: '{% endif %}'.repeat(conditionalStack.length) }
1011
+ : null;
1012
+ for (let p = 0; p < parts.length; p++) {
1013
+ const part = parts[p].trim();
1014
+ if (part === '' || part === undefined)
1015
+ continue;
1016
+ // Check if this part is 'a' prefix (from splitting "a| content" on |)
1017
+ // Also handle inline "text a|" where part ends with ' a'
1018
+ if ((part === 'a' || part.endsWith(' a')) && p + 1 < parts.length) {
1019
+ // If part has text before ' a', add it as a regular cell first
1020
+ if (part !== 'a') {
1021
+ const textContent = part.slice(0, -2).trim();
1022
+ if (textContent) {
1023
+ const converted = convertCellString(textContent);
1024
+ addCell(converted, stackWrapper);
1025
+ cellsAddedThisLine++;
1026
+ checkRowComplete();
1027
+ }
1028
+ }
1029
+ flushAsciidocCell();
1030
+ checkRowComplete();
1031
+ inAsciidocCell = true;
1032
+ asciidocCellLines = [];
1033
+ asciidocCellWrapper = stackWrapper;
1034
+ const restContent = parts[p + 1].trim();
1035
+ if (restContent) {
1036
+ asciidocCellLines.push(restContent);
1037
+ }
1038
+ cellsAddedThisLine++;
1039
+ p++; // Skip the next part since we consumed it
1040
+ }
1041
+ else if (trimmed.startsWith('a|') && p === 0) {
1042
+ // Line starts with a| (e.g., "a| \n* item1\n* item2")
1043
+ flushAsciidocCell();
1044
+ checkRowComplete();
1045
+ inAsciidocCell = true;
1046
+ asciidocCellLines = [];
1047
+ asciidocCellWrapper = stackWrapper;
1048
+ if (part.substring(1).trim()) {
1049
+ asciidocCellLines.push(part.substring(1).trim());
1050
+ }
1051
+ cellsAddedThisLine++;
1052
+ }
1053
+ else {
1054
+ // Regular cell
1055
+ const converted = convertCellString(part);
1056
+ addCell(converted, stackWrapper);
1057
+ cellsAddedThisLine++;
1058
+ checkRowComplete();
1059
+ }
1060
+ }
1061
+ // If this line added multiple cells and we don't know column count yet,
1062
+ // infer it from this line (e.g., "| Type | Description | Notes" = 3 columns)
1063
+ if (columnCount === 0 && cellsAddedThisLine > 1) {
1064
+ columnCount = cellsAddedThisLine;
1065
+ checkRowComplete();
1066
+ }
1067
+ }
1068
+ }
1069
+ // Flush any remaining a| cell and row
1070
+ flushAsciidocCell();
1071
+ if (currentRow.length > 0) {
1072
+ outputRow();
1073
+ }
1074
+ if (bodyStarted) {
1075
+ result.push(M + '</tbody>');
1076
+ }
1077
+ result.push(M + '</table>');
1078
+ return result.join('\n');
1079
+ }
1080
+ /**
1081
+ * Preprocesses AsciiDoc content to convert complex tables to HTML.
1082
+ * Complex tables include those with conditionals, colspan, rowspan, or nested content.
1083
+ *
1084
+ * @param {string} content - AsciiDoc content
1085
+ * @returns {string} Content with complex tables converted to HTML
1086
+ */
1087
+ function convertComplexTablesToHtml(content) {
1088
+ const lines = content.split('\n');
1089
+ const result = [];
1090
+ let i = 0;
1091
+ while (i < lines.length) {
1092
+ const line = lines[i];
1093
+ const trimmed = line.trim();
1094
+ // Check for table block attributes followed by table start
1095
+ // AsciiDoc tables use |=== with 3 or more = signs
1096
+ if (/^\|={3,}$/.test(trimmed) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
1097
+ // Look for table start
1098
+ let tableStart = i;
1099
+ let blockAttrs = null;
1100
+ if (trimmed.startsWith('[')) {
1101
+ // This might be block attributes before a table
1102
+ blockAttrs = trimmed;
1103
+ // Look ahead for |===
1104
+ if (i + 1 < lines.length) {
1105
+ const nextLine = lines[i + 1].trim();
1106
+ if (/^\|={3,}$/.test(nextLine)) {
1107
+ tableStart = i + 1;
1108
+ i++; // Skip to |===
1109
+ }
1110
+ else {
1111
+ // Not a table, preserve the line
1112
+ result.push(line);
1113
+ i++;
1114
+ continue;
1115
+ }
1116
+ }
1117
+ else {
1118
+ result.push(line);
1119
+ i++;
1120
+ continue;
1121
+ }
1122
+ }
1123
+ // We're at a |=== line
1124
+ if (!/^\|={3,}$/.test(lines[i].trim())) {
1125
+ result.push(line);
1126
+ i++;
1127
+ continue;
1128
+ }
1129
+ // Find the end of the table
1130
+ const tableLines = [];
1131
+ i++; // Move past opening |===
1132
+ while (i < lines.length && !/^\|={3,}$/.test(lines[i].trim())) {
1133
+ tableLines.push(lines[i]);
1134
+ i++;
1135
+ }
1136
+ // Check if this table needs HTML conversion
1137
+ if (tableNeedsHtmlConversion(tableLines)) {
1138
+ // Parse block attributes for table options
1139
+ const options = {};
1140
+ if (blockAttrs) {
1141
+ const colsMatch = blockAttrs.match(/cols="([^"]+)"/);
1142
+ if (colsMatch)
1143
+ options.colSpec = colsMatch[1];
1144
+ options.hasHeader = blockAttrs.includes('header') && !blockAttrs.includes('noheader');
1145
+ }
1146
+ // Convert to HTML
1147
+ const htmlTable = convertTableToHtml(tableLines, options);
1148
+ // Add block title if present (look back for .Title line)
1149
+ if (result.length > 0 && result[result.length - 1].startsWith('.') &&
1150
+ !result[result.length - 1].startsWith('..')) {
1151
+ const title = result.pop().substring(1);
1152
+ result.push(`**${title}**`);
1153
+ result.push('');
1154
+ }
1155
+ result.push(htmlTable);
1156
+ // CommonMark requires a blank line after an HTML block before markdown resumes
1157
+ // Mark as passthrough so the main engine preserves it
1158
+ result.push(HTML_PASSTHROUGH_MARKER);
1159
+ }
1160
+ else {
1161
+ // Keep as-is (will be converted to markdown table later)
1162
+ if (blockAttrs) {
1163
+ result.push(blockAttrs);
1164
+ }
1165
+ result.push('|===');
1166
+ result.push(...tableLines);
1167
+ result.push('|===');
1168
+ }
1169
+ i++; // Move past closing |===
1170
+ }
1171
+ else {
1172
+ result.push(line);
1173
+ i++;
1174
+ }
1175
+ }
1176
+ return result.join('\n');
1177
+ }
1178
+ /**
1179
+ * Converts AsciiDoc conditional directives to standard Jinja2 format.
1180
+ * Handles ifdef, ifndef, ifeval, and endif directives.
1181
+ * Also converts AsciiDoc attribute assignments (:attr:) to Jinja2 set statements.
1182
+ * Intelligently manages whitespace stripping to preserve significant blank lines.
1183
+ *
1184
+ * @param {string} content - AsciiDoc content
1185
+ * @returns {string} Content with conditionals converted to Jinja2 format
1186
+ */
1187
+ function convertConditionals(content) {
1188
+ const lines = content.split('\n');
1189
+ const result = [];
1190
+ const conditionalStack = []; // Track conditional nesting
1191
+ for (let i = 0; i < lines.length; i++) {
1192
+ const line = lines[i];
1193
+ const trimmed = line.trim();
1194
+ // Handle ifdef directive - convert to: {% if name %} or {% if name1 or name2 %} or {% if name1 and name2 %}
1195
+ const ifdefMatch = trimmed.match(/^ifdef::([^\[]+)\[\]$/);
1196
+ if (ifdefMatch) {
1197
+ const [, names] = ifdefMatch;
1198
+ // Parse attributes with both OR (,) and AND (+) operators
1199
+ // Examples: "attr1,attr2" → "attr1 or attr2"
1200
+ // "attr1+attr2" → "attr1 and attr2"
1201
+ // "attr1,attr2+attr3" → "attr1 or (attr2 and attr3)"
1202
+ const orGroups = names.split(',').map(group => {
1203
+ const andAttrs = group.trim().split('+').map(a => a.trim().replace(/-/g, '_'));
1204
+ return andAttrs.length === 1 ? andAttrs[0] : `(${andAttrs.join(' and ')})`;
1205
+ });
1206
+ const condition = orGroups.join(' or ');
1207
+ const leftStrip = isInlineOpeningContext(lines, i) ? '-' : '';
1208
+ result.push(`{%${leftStrip} if ${condition} %}`);
1209
+ conditionalStack.push('if');
1210
+ continue;
1211
+ }
1212
+ // Handle ifndef directive - convert to: {% if not name %} or {% if not (name1 or name2) %}
1213
+ const ifndefMatch = trimmed.match(/^ifndef::([^\[]+)\[\]$/);
1214
+ if (ifndefMatch) {
1215
+ const [, names] = ifndefMatch;
1216
+ // Parse attributes with both OR (,) and AND (+) operators
1217
+ const orGroups = names.split(',').map(group => {
1218
+ const andAttrs = group.trim().split('+').map(a => a.trim().replace(/-/g, '_'));
1219
+ return andAttrs.length === 1 ? andAttrs[0] : `(${andAttrs.join(' and ')})`;
1220
+ });
1221
+ const leftStrip = isInlineOpeningContext(lines, i) ? '-' : '';
1222
+ if (orGroups.length === 1 && !orGroups[0].includes('(')) {
1223
+ // Single attribute without AND, no parentheses needed
1224
+ result.push(`{%${leftStrip} if not ${orGroups[0]} %}`);
1225
+ }
1226
+ else {
1227
+ const condition = orGroups.join(' or ');
1228
+ result.push(`{%${leftStrip} if not (${condition}) %}`);
1229
+ }
1230
+ conditionalStack.push('if');
1231
+ continue;
1232
+ }
1233
+ // Handle ifeval directive - convert to: {% if expression %}
1234
+ const ifevalMatch = trimmed.match(/^ifeval::\[(.+)\]$/);
1235
+ if (ifevalMatch) {
1236
+ const [, expression] = ifevalMatch;
1237
+ // Parse and convert the expression to standard Jinja2 format
1238
+ // Convert AsciiDoc attribute references {var} or "{ var}" to just var
1239
+ const jinja2Expr = expression.replace(/"\{([a-zA-Z0-9_-]+)\}"/g, '$1');
1240
+ const leftStrip = isInlineOpeningContext(lines, i) ? '-' : '';
1241
+ result.push(`{%${leftStrip} if ${jinja2Expr} %}`);
1242
+ conditionalStack.push('if');
1243
+ continue;
1244
+ }
1245
+ // Handle endif directive (matches endif::[] or endif::name[] formats)
1246
+ // Always output standard {% endif %} tag
1247
+ if (trimmed.match(/^endif::/)) {
1248
+ if (conditionalStack.length > 0) {
1249
+ conditionalStack.pop();
1250
+ const leftStrip = isInlineClosingContext(lines, i) ? '-' : '';
1251
+ result.push(`{%${leftStrip} endif %}`);
1252
+ continue;
1253
+ }
1254
+ // If no conditional on stack, preserve the line (shouldn't happen in valid AsciiDoc)
1255
+ console.warn(`Warning: ${trimmed} without matching conditional directive`);
1256
+ continue;
1257
+ }
1258
+ // Convert attribute assignments to Jinja2 set statements
1259
+ // Look ahead to determine if we should strip whitespace on the right
1260
+ let stripRight = true;
1261
+ // Find the next non-empty line
1262
+ for (let j = i + 1; j < lines.length; j++) {
1263
+ const nextLine = lines[j].trim();
1264
+ if (nextLine) {
1265
+ // If next non-empty line is NOT a template directive, preserve whitespace
1266
+ // Template directives don't produce output, so stripping before them is safe
1267
+ // Regular content produces output, so we preserve whitespace before it
1268
+ stripRight = isTemplateDirective(lines[j]);
1269
+ break;
1270
+ }
1271
+ }
1272
+ const jinja2Set = convertAttributeAssignment(line, stripRight);
1273
+ if (jinja2Set) {
1274
+ result.push(jinja2Set);
1275
+ continue;
1276
+ }
1277
+ // Convert link macros that contain attribute references to markdown format
1278
+ // Must happen before attribute references are expanded, since {attr} → {{ attr }}
1279
+ // would break the URL pattern (spaces in URL)
1280
+ let processedLine = line;
1281
+ if (processedLine.includes('link:') && processedLine.includes('{')) {
1282
+ processedLine = processedLine.replace(/link:([^\s\[]*\{[^}]+\}[^\s\[]*)\[([^\]]*)\]/g, (_, url, text) => {
1283
+ return `[${text}](${url})`;
1284
+ });
1285
+ }
1286
+ // Preserve all other lines as-is
1287
+ result.push(processedLine);
1288
+ }
1289
+ // Auto-close unclosed conditionals at end of file with warning
1290
+ if (conditionalStack.length > 0) {
1291
+ console.warn(`Warning: ${conditionalStack.length} unclosed conditional directive(s) - auto-closing`);
1292
+ // Add closing endif tags for each unclosed conditional
1293
+ for (let i = 0; i < conditionalStack.length; i++) {
1294
+ result.push('{% endif %}');
1295
+ }
1296
+ }
1297
+ return result.join('\n');
1298
+ }
1299
+ /**
1300
+ * Cleans up redundant AsciiDoc continuation markers (+) that appear before list items.
1301
+ * These markers are often left over after include resolution and can cause formatting issues.
1302
+ *
1303
+ * Removes:
1304
+ * - Standalone + on a line before a list item (. or *)
1305
+ * - + with optional whitespace before list items
1306
+ *
1307
+ * @param {string} content - AsciiDoc content (after include resolution)
1308
+ * @returns {string} Content with cleaned up continuation markers
1309
+ */
1310
+ function cleanupContinuationMarkers(content) {
1311
+ const lines = content.split('\n');
1312
+ const cleaned = [];
1313
+ for (let i = 0; i < lines.length; i++) {
1314
+ const line = lines[i];
1315
+ const trimmed = line.trim();
1316
+ // Check if this is a standalone continuation marker
1317
+ if (trimmed === '+') {
1318
+ // Look ahead to see if the next non-empty line is a list item
1319
+ let nextIdx = i + 1;
1320
+ while (nextIdx < lines.length && lines[nextIdx].trim() === '') {
1321
+ nextIdx++;
1322
+ }
1323
+ if (nextIdx < lines.length) {
1324
+ const nextLine = lines[nextIdx].trim();
1325
+ // Check if next line is a list item (. or numbered, or *, or definition list ::)
1326
+ const isListItem = /^(\d+\.|\.|\*+|[^:]+::)\s/.test(nextLine);
1327
+ if (isListItem) {
1328
+ // Skip this continuation marker - it's redundant before a list item
1329
+ continue;
1330
+ }
1331
+ }
1332
+ }
1333
+ cleaned.push(line);
1334
+ }
1335
+ return cleaned.join('\n');
1336
+ }
1337
+ export default function downdoc(asciidoc, { attributes: initialAttrs = {}, basePath = null, cleanupContinuation = true, nunjucksIncludes = true, dlistFormat = 'markdown', admonitionFormat = 'html' } = {}) {
1338
+ // Resolve includes if basePath is provided
1339
+ if (basePath) {
1340
+ asciidoc = resolveIncludes(asciidoc, basePath, 0, nunjucksIncludes);
1341
+ }
1342
+ // Convert complex tables (with conditionals) to HTML format
1343
+ // This must run before convertConditionals so table conditionals are handled inline
1344
+ if (nunjucksIncludes) {
1345
+ asciidoc = convertComplexTablesToHtml(asciidoc);
1346
+ }
1347
+ // Convert AsciiDoc conditionals to Minja format (always enabled with Nunjucks mode)
1348
+ if (nunjucksIncludes) {
1349
+ asciidoc = convertConditionals(asciidoc);
1350
+ }
1351
+ // Clean up continuation markers if enabled (default: true)
1352
+ // Removes redundant + before list items in both inline and nunjucks modes
1353
+ if (cleanupContinuation) {
1354
+ asciidoc = cleanupContinuationMarkers(asciidoc);
1355
+ }
1356
+ const attrs = new Map(Object.entries(Object.assign({}, ATTRIBUTES, initialAttrs)));
1357
+ const lines = asciidoc.split(attrs.delete('doctitle') ? '\n' : '\n');
1358
+ if (lines[lines.length - 1] === '')
1359
+ lines.pop();
1360
+ let inContainer, inHeader, inList, inPara, inTable, indent, inDlistHtml, inDlistPandoc, pandocDlistIndent;
1361
+ inHeader = (inContainer = inPara = (indent = '') || false) || asciidoc[0] === '=' || !!~asciidoc.indexOf('\n= ');
1362
+ inDlistHtml = false; // Track if we're inside an HTML definition list
1363
+ inDlistPandoc = false; // Track if we're inside a Pandoc definition list
1364
+ pandocDlistIndent = ''; // Indent prefix for content inside Pandoc definition list
1365
+ let pandocDlistPendingColon = false; // Defer ': ' prefix until first content block
1366
+ let blockAttrs, blockTitle, chr0, grab, hardbreakNext, listStack, match, next, subs, style, verbatim, currentHeadingLevel;
1367
+ const [containerStack, nrefs, refs, skipStack, undef] = [(listStack = []).slice(), new Map(), new Map(), [], () => { }];
1368
+ return lines
1369
+ .reduce((accum, line, idx) => {
1370
+ while ((grab = match = next = style = subs = undefined) === undefined) {
1371
+ if (skipStack.length && (match = skipStack[skipStack.length - 1] || (line === 'endif::[]' && line))) {
1372
+ if (line === match)
1373
+ return undef(skipStack.pop()) || accum;
1374
+ if (match === 'endif::[]' && line.startsWith('if') && /^ifn?def::.+\[\]$/.test(line))
1375
+ skipStack.push(match);
1376
+ return accum;
1377
+ }
1378
+ // Handle Jinja template directives - pass through unchanged without affecting parser state
1379
+ // These are generated by convertConditionals() and should not be treated as content
1380
+ // IMPORTANT: Template directives should NEVER be indented, even in list context
1381
+ const trimmedLine = line.trim();
1382
+ if (trimmedLine.startsWith('{%') || trimmedLine.startsWith('{{')) {
1383
+ inHeader = false; // Exit header mode when we encounter template directives
1384
+ // Write any pending block title before the directive
1385
+ blockTitle &&= writeBlockTitle(accum, blockTitle, blockAttrs, attrs, refs, currentHeadingLevel);
1386
+ accum.push(trimmedLine); // No indentation for template directives
1387
+ return accum;
1388
+ }
1389
+ // Handle HTML passthrough content - pass through unchanged
1390
+ // This is used for complex tables that have been converted to HTML
1391
+ if (trimmedLine.startsWith(HTML_PASSTHROUGH_MARKER)) {
1392
+ inHeader = false;
1393
+ blockTitle &&= writeBlockTitle(accum, blockTitle, blockAttrs, attrs, refs, currentHeadingLevel);
1394
+ // Remove the marker and output the HTML
1395
+ accum.push(trimmedLine.substring(HTML_PASSTHROUGH_MARKER.length));
1396
+ return accum;
1397
+ }
1398
+ if (!line && !inContainer.verbatim) {
1399
+ if (inTable || (inHeader = inPara = false))
1400
+ return accum;
1401
+ // Close HTML definition list on empty line
1402
+ if (inDlistHtml) {
1403
+ accum.push(indent + '</dl>');
1404
+ accum.push(''); // Blank line after </dl>
1405
+ inDlistHtml = false;
1406
+ }
1407
+ // Close Pandoc definition list on empty line (matches HTML dlist behavior above)
1408
+ // New dlist items will re-set inDlistPandoc; continuation after blank line
1409
+ // requires AsciiDoc '+' marker which uses the normal list continuation mechanism
1410
+ // Don't reset if pendingColon is true — blank line is between term and definition content
1411
+ // Don't reset inside a container (open block, etc.) — blank lines within containers
1412
+ // attached to a dlist via + should not end the dlist
1413
+ if (inDlistPandoc && !pandocDlistPendingColon && !inContainer) {
1414
+ inDlistPandoc = false;
1415
+ pandocDlistIndent = '';
1416
+ }
1417
+ inList ? (inPara = undefined) : (line = indent = inContainer.childIndent || '') && (line = line.trimEnd());
1418
+ // Preserve blockAttrs and blockTitle across single empty line, clear verbatim
1419
+ verbatim = verbatim?.close();
1420
+ return accum[accum.length - 1] && (accum[accum.length] = line) ? accum : accum;
1421
+ }
1422
+ else if (((grab = (chr0 = line[0]) === '\\') || (chr0 === 'i' && line[1] !== 'm')) &&
1423
+ ((grab && line === '\\endif::[]') || (line[line.length - 1] === ']' && ~line.indexOf('::') &&
1424
+ (match = PreprocessorDirectiveRx.exec(line)))) && !(line = grab ? line.substring(1) : undefined)) {
1425
+ if (match[1]) {
1426
+ const [, , negated, name, text, drop = attrs.has(name) ? !!negated : !negated] = match;
1427
+ if (text ? (drop ? false : (line = text)) : !skipStack.push(drop && 'endif::[]'))
1428
+ continue; // redo
1429
+ }
1430
+ }
1431
+ else if (inContainer.verbatim) {
1432
+ if (line === inContainer.delimiter || line.trimEnd() === inContainer.delimiter) {
1433
+ ;
1434
+ ({ cap: line, indent, inList, listStack } = inContainer);
1435
+ if (inContainer.outdent && (grab = inContainer.outdent + indent.length) && ~(match = inContainer.at)) {
1436
+ for (let i = match, l = accum.length; ++i < l;)
1437
+ accum[i] = (indent + accum[i].substring(grab)).trimEnd();
1438
+ }
1439
+ inContainer = containerStack.length ? containerStack.pop() : false;
1440
+ }
1441
+ else if ((match = line.length)) {
1442
+ inContainer.outdent &&= Math.min(match - line.trimStart().length, inContainer.outdent);
1443
+ if (match > 2 && (~line.indexOf('{') || line[match - 1] === '>'))
1444
+ subs = inContainer.subs;
1445
+ }
1446
+ }
1447
+ else if (!inHeader && ((grab = line.trimEnd()) in DELIMS || line in DELIMS)) {
1448
+ // Normalize delimiter by trimming trailing whitespace (some files have tabs after ----)
1449
+ const delimiter = grab in DELIMS ? grab : line;
1450
+ if (inPara !== false)
1451
+ inPara = !(listStack = (inList = undef((indent = inContainer.childIndent || ''))) || []);
1452
+ const opening = inContainer === false || (delimiter !== inContainer.delimiter && containerStack.push(inContainer))
1453
+ ? (inContainer = { delimiter, indent, childIndent: indent, inList, listStack })
1454
+ : undefined;
1455
+ if ((grab = DELIMS[delimiter]) === 'v') {
1456
+ inContainer.subs = (inContainer.verbatim = !!(attrs.coseq = 1)) && ['callouts'];
1457
+ if (blockAttrs) {
1458
+ style = ((style = blockAttrs.get(1) || (line === '----' || line === '-----' ? 'source' : undefined)) === 'source')
1459
+ ? blockAttrs.get(2) || attrs.get('source-language')
1460
+ : style === 'listing' || style === 'literal' ? undefined : style;
1461
+ if (blockAttrs.get('indent') === '0')
1462
+ Object.assign(inContainer, { at: accum.length, outdent: Infinity });
1463
+ if (blockAttrs.get('subs')?.includes('attributes'))
1464
+ inContainer.subs.push('attributes');
1465
+ blockTitle &&= writeBlockTitle(accum, blockTitle, blockAttrs, attrs, refs, currentHeadingLevel);
1466
+ }
1467
+ else if (line === '----' || line === '-----')
1468
+ style = attrs.get('source-language');
1469
+ line = style ? (inContainer.cap = '```') + style : (inContainer.cap = '```');
1470
+ }
1471
+ else if (grab === 'p' && (inContainer.verbatim = true)) {
1472
+ line = blockAttrs && blockAttrs.get(1) === 'stem' ? (inContainer.cap = '```') + 'math' : undefined;
1473
+ blockTitle = undefined;
1474
+ }
1475
+ else if (opening === undefined) {
1476
+ ;
1477
+ ({ cap: line, indent, inList, listStack } = inContainer);
1478
+ inTable = blockTitle = undefined;
1479
+ inContainer = containerStack.length ? containerStack.pop() : false;
1480
+ }
1481
+ else if (grab === 't') {
1482
+ inTable = { header: blockAttrs?.has('header-option') || blockAttrs?.get('options')?.includes('header') || (!blockAttrs?.has('noheader-option') && undefined) };
1483
+ if (blockAttrs !== (line = undefined) && (grab = blockAttrs.get('cols'))) {
1484
+ const cols = (!~grab.indexOf('*') && grab.split(/,|;/)) || grab.split(/,|;/)
1485
+ .reduce((a, c) => a.push.apply(a, ~c.indexOf('*') ? Array(parseInt(c, 10)).fill(c) : [c]) && a, []);
1486
+ (inTable.div = ~grab.indexOf('<') || ~grab.indexOf('^') || ~grab.indexOf('>')
1487
+ ? cols.reduce((buf, c) => buf + TDIV[/(?<!\.)[<^>]|$/.exec(c)[0]], '') + '|'
1488
+ : TDIV[''].repeat(cols.length) + '|') && (inTable.cols = cols.length);
1489
+ }
1490
+ }
1491
+ else if (line === '____') {
1492
+ indent = inContainer.childIndent += '> ';
1493
+ if ((grab = blockAttrs && blockAttrs.get(2)))
1494
+ inContainer.cap = '>\n' + indent + '\u2014 ' + grab;
1495
+ line = blockTitle &&= writeBlockTitle(accum, blockTitle, blockAttrs, attrs, refs, currentHeadingLevel);
1496
+ }
1497
+ else if (line === '====' && blockAttrs) {
1498
+ if ((style = blockAttrs.get(1)) in ADMONS) {
1499
+ const admonType = ADMON_TYPES[style];
1500
+ const title = blockTitle ? blockTitle.text : '';
1501
+ const id = blockAttrs.has('id') ? blockAttrs.get('id') : '';
1502
+ const isCollapsible = blockAttrs.has('collapsible-option');
1503
+ const currentIndent = indent || '';
1504
+ // Include pandocDlistIndent for markers pushed directly to accum,
1505
+ // since the normal line output (which prepends pandocDlistIndent) is bypassed
1506
+ const fullIndent = (pandocDlistIndent || '') + currentIndent;
1507
+ if (admonitionFormat === 'mkdocs') {
1508
+ // MkDocs: !!! type "title" or ??? for collapsible
1509
+ const prefix = isCollapsible ? '???' : '!!!';
1510
+ accum.push(''); // Blank line before
1511
+ accum.push(fullIndent + prefix + ' ' + admonType + (title ? ' "' + title + '"' : ''));
1512
+ accum.push(''); // Blank line after opening
1513
+ indent = inContainer.childIndent = currentIndent + ' ';
1514
+ inContainer.cap = '\n';
1515
+ line = undefined;
1516
+ }
1517
+ else if (admonitionFormat === 'docusaurus') {
1518
+ // Docusaurus: :::type or :::type[title]
1519
+ accum.push(''); // Blank line before
1520
+ accum.push(fullIndent + ':::' + admonType + (title ? '[' + title + ']' : ''));
1521
+ accum.push(''); // Blank line after opening
1522
+ inContainer.cap = '\n' + fullIndent + ':::\n';
1523
+ line = undefined;
1524
+ }
1525
+ else if (admonitionFormat === 'github') {
1526
+ // GitHub: > [!TYPE]
1527
+ accum.push(''); // Blank line before
1528
+ accum.push(fullIndent + '> [!' + style + ']');
1529
+ indent = inContainer.childIndent = currentIndent + '> ';
1530
+ inContainer.cap = '\n';
1531
+ line = undefined;
1532
+ }
1533
+ else {
1534
+ // HTML (default): <dl><dt>...</dt><dd>...</dd></dl>
1535
+ line = '\n' + fullIndent + '<dl><dt><strong>' + (id ? '<a name="' + id + '"></a>' : '') +
1536
+ ADMONS[style] + ' ' + style + (title ? ': ' + title : '') + '</strong></dt><dd>\n';
1537
+ inContainer.cap = '\n' + fullIndent + '</dd></dl>\n';
1538
+ }
1539
+ blockTitle = undefined;
1540
+ }
1541
+ else if (blockAttrs.has('collapsible-option')) {
1542
+ line = attrs.get('markdown-collapsible-variant') === 'spoiler' && (grab = 'spoiler')
1543
+ ? (inContainer.cap = '```') + (blockTitle ? grab + ' ' + applySubs.call(attrs, blockTitle.text) : grab)
1544
+ : (inContainer.cap = '</details>') && (blockAttrs.has('open-option') ? '<details open>' : '<details>') +
1545
+ '\n' + indent + '<summary>' + (blockTitle ? blockTitle.text : 'Details') + '</summary>\n';
1546
+ blockTitle = undefined;
1547
+ }
1548
+ else
1549
+ line = blockTitle &&= writeBlockTitle(accum, blockTitle, blockAttrs, attrs, refs, currentHeadingLevel);
1550
+ }
1551
+ else
1552
+ line = blockTitle &&= writeBlockTitle(accum, blockTitle, blockAttrs, attrs, refs, currentHeadingLevel);
1553
+ if (opening !== (blockAttrs = undefined))
1554
+ listStack = (inList = undefined) || [];
1555
+ }
1556
+ else {
1557
+ let _chr0, _line, indented;
1558
+ if (!(indented = chr0 === ' '))
1559
+ _line = line;
1560
+ if (inPara === undefined && !(inPara = false) && (_chr0 = indented ? (_line = line.trimStart())[0] : chr0)) {
1561
+ // Don't clear list state for continuation markers (+) - they'll be handled below
1562
+ if (!(line.trim() === '+' && inList)) {
1563
+ isAnyListItem(_chr0, _line) && inList
1564
+ ? (accum.length && !accum[accum.length - 1].trim() && accum.pop())
1565
+ : !indented && (listStack = (inList = undef((indent = inContainer.childIndent || ''))) || []);
1566
+ }
1567
+ }
1568
+ if (chr0 === '/' && line[1] === '/') {
1569
+ line = line === '////' ? undef((inPara = !(skipStack.push(line)))) : undefined;
1570
+ }
1571
+ else if (!inPara && chr0 === '=' && (match = isHeading(line, inContainer === false, blockAttrs))) {
1572
+ // Heading ends any active Pandoc definition list
1573
+ inDlistPandoc = false;
1574
+ pandocDlistIndent = '';
1575
+ let [marker, title, id, autoId] = match;
1576
+ if (inHeader) {
1577
+ if (marker.length > 1 || (blockAttrs && blockAttrs.get(1) === 'discrete') || attrs.has('doctitle')) {
1578
+ if (!(inHeader = false) && accum.length)
1579
+ return accum;
1580
+ continue; // redo
1581
+ }
1582
+ attrs.set('doctitle', title);
1583
+ if ((id = blockAttrs?.get('id')))
1584
+ autoId = title.toLowerCase().replace(/[^a-z0-9_ -]/g, '').split(' ').filter(Boolean).join('-');
1585
+ }
1586
+ else {
1587
+ grab = (title = attributes.call(attrs, title)).toLowerCase().replace(/[^a-z0-9_ -]/g, '').split(' ').filter(Boolean);
1588
+ autoId = grab.join('-');
1589
+ id = blockAttrs?.get('id') || attrs.get('idprefix') + grab.join(attrs.get('idseparator'));
1590
+ }
1591
+ if (id)
1592
+ refs.set(title, refs.set(id, { autoId, reftext: blockAttrs?.get('reftext'), title }).get(id));
1593
+ currentHeadingLevel = marker.length; // Track current heading level for section titles
1594
+ blockAttrs = blockTitle = undefined;
1595
+ // Convert attribute references in ID to Jinja2 format (e.g., {context} → {{ context }})
1596
+ const convertedId = id ? attributes.call(attrs, id) : null;
1597
+ line = '#'.repeat(marker.length) + ' ' + title + (convertedId ? ` {id="${convertedId}"}` : '');
1598
+ }
1599
+ else if (!inPara && chr0 === ':' && (match = AttributeEntryRx.exec(line))) {
1600
+ const [, del, name, val = ''] = (line = undefined) || match;
1601
+ if (!(name in initialAttrs))
1602
+ del ? attrs.delete(name) : attrs.set(name, val && attributes.call(attrs, val));
1603
+ }
1604
+ else if (chr0 === '[' && line[line.length - 1] === ']') {
1605
+ if (verbatim && !(inPara = verbatim = verbatim.close()))
1606
+ continue; // redo
1607
+ blockAttrs = parseAttrlist(line.substring(1, line.length - 1), blockAttrs);
1608
+ line = (inPara = false) || undefined;
1609
+ }
1610
+ else if (inHeader) {
1611
+ if ((inHeader = attrs.has('doctitle'))) {
1612
+ if (!attrs.has('author') && AuthorInfoLineRx.test(line)) {
1613
+ const authors = line.split('; ').map((it) => it.split(' <')[0]);
1614
+ attrs.set('author', authors[0]).set('authors', authors.join(', '));
1615
+ }
1616
+ else if (!('revdate' in attrs) && !('revnumber' in attrs) && (match = RevisionInfoLineRx.exec(line))) {
1617
+ const [, revnumber, revdate_, revdate = revdate_] = match;
1618
+ (revnumber ? attrs.set('revnumber', revnumber) : true) && revdate && attrs.set('revdate', revdate);
1619
+ }
1620
+ else
1621
+ inHeader = false;
1622
+ }
1623
+ if (!inHeader && !accum.length)
1624
+ continue; // redo
1625
+ line = undefined;
1626
+ }
1627
+ else if (inTable) {
1628
+ const row = inTable.row;
1629
+ const cells = ~line.indexOf('|', 1)
1630
+ ? line.split(CellDelimiterRx)
1631
+ : chr0 === '|' ? ['', line.substring(line[1] === ' ' ? 2 : 1)] : [line];
1632
+ if (row) {
1633
+ if (cells[0]) {
1634
+ if (row.length && (row.wrapped = true)) {
1635
+ row[row.length - 1] = ((grab = row[row.length - 1]) && hardbreak(grab, ' +\n') + ' ') + cells[0];
1636
+ }
1637
+ else {
1638
+ line = (grab = accum[accum.length - 1].split('\n'))[0].substring(0, grab[0].length - 2).trimEnd();
1639
+ line = hardbreak(line, '<br>') + ' ' + applySubs.call(attrs, cells[0]) + ' |';
1640
+ accum[accum.length - 1] = grab.length > 1 ? (grab[0] = line) && grab.join('\n') : line;
1641
+ }
1642
+ }
1643
+ if ((cells.length === 1 ? row.length : row.push.apply(row, cells.slice(1))) < inTable.cols)
1644
+ return accum;
1645
+ line = '| ' + applySubs.call(attrs, row.splice(0, inTable.cols).join(' | ')) + ' |';
1646
+ if (row.wrapped && !(row.wrapped = false) && ~line.indexOf(' +\n'))
1647
+ line = line.replace(/ \+\n/g, '<br>');
1648
+ // If this completes a multi-line header row, append the divider
1649
+ if (inTable.headerPending) {
1650
+ line += '\n' + indent + (inTable.div || TDIV[''].repeat(inTable.cols) + '|');
1651
+ inTable.headerPending = false;
1652
+ }
1653
+ if (row.length && accum.push(indent + line) && (line = '|' + row.splice(0).join(' |')))
1654
+ continue; // redo
1655
+ }
1656
+ else {
1657
+ const cols = (inTable.row = []) && (inTable.cols ??= cells.length - 1);
1658
+ if (!(inTable.header ??= cols === cells.length - 1 && lines[idx + 1] === '' && lines[idx - 1] !== '')) {
1659
+ blockTitle &&= writeBlockTitle(accum, blockTitle, blockAttrs, attrs, refs, currentHeadingLevel);
1660
+ accum.push(indent + '| '.repeat(cols) + '|', indent + (inTable.div || TDIV[''].repeat(cols) + '|'));
1661
+ continue; // redo
1662
+ }
1663
+ subs = NORMAL_SUBS;
1664
+ // If header cells span multiple lines, accumulate them like body rows
1665
+ if (cells.length - 1 < cols) {
1666
+ inTable.headerPending = true;
1667
+ inTable.row.push.apply(inTable.row, cells.slice(1));
1668
+ return accum;
1669
+ }
1670
+ line = '| ' + cells.slice(1).join(' | ') + ' |\n' + indent + (inTable.div || TDIV[''].repeat(cols) + '|');
1671
+ }
1672
+ }
1673
+ else if (line.trim() === '+' && (inList || listStack.length || inDlistPandoc)) {
1674
+ if (inList || listStack.length) {
1675
+ const listRef = inList || listStack[listStack.length - 1];
1676
+ ({ indent: line, childIndent: indent } = listRef);
1677
+ // Reset Pandoc dlist state if returning from dlist to ordered list via continuation
1678
+ if (!inList && inDlistPandoc) {
1679
+ inDlistPandoc = false;
1680
+ pandocDlistIndent = '';
1681
+ }
1682
+ }
1683
+ verbatim = (inPara = false) || verbatim?.close();
1684
+ // Skip outputting the continuation marker itself - it's just for attaching blocks
1685
+ // The actual indentation will be handled by the content that follows
1686
+ return accum;
1687
+ }
1688
+ else if ((!inPara || inList || inDlistHtml || inDlistPandoc) && (_chr0 ??= indented ? (_line = line.trimStart())[0] : chr0) &&
1689
+ (match = isAnyListItem(_chr0, _line, 'exec'))) {
1690
+ let [, marker, text, desc, dlist] = match;
1691
+ if (dlist !== undefined && (dlist = text)) { // marker and text are swapped for dlist item
1692
+ const isQanda = (blockAttrs && blockAttrs.get(1) === 'qanda') || (inList && inList.dlist === 'qanda');
1693
+ // Handle non-markdown definition list formats
1694
+ if (!isQanda && dlistFormat === 'html') {
1695
+ // Preserve ordered list context when entering dlist (push to stack instead of clearing)
1696
+ if (inList && !inList.dlist) {
1697
+ listStack.push(inList);
1698
+ inList = undefined;
1699
+ }
1700
+ // HTML format: <dl><dt>term</dt><dd>desc</dd></dl>
1701
+ const htmlIndent = indent || '';
1702
+ verbatim = verbatim?.close();
1703
+ inPara = true;
1704
+ if (!inDlistHtml) {
1705
+ accum.push(htmlIndent.trimEnd() || ''); // Blank line before <dl>
1706
+ accum.push(htmlIndent + '<dl>');
1707
+ inDlistHtml = true;
1708
+ }
1709
+ // Apply attribute substitution to term and description
1710
+ const htmlTerm = attributes.call(attrs, marker);
1711
+ const htmlDesc = desc ? attributes.call(attrs, desc) : '';
1712
+ // Output dt/dd - desc may be empty (description on next line)
1713
+ if (desc) {
1714
+ accum.push(htmlIndent + '<dt>' + htmlTerm + '</dt><dd>' + htmlDesc + '</dd>');
1715
+ }
1716
+ else {
1717
+ accum.push(htmlIndent + '<dt>' + htmlTerm + '</dt><dd>');
1718
+ hardbreakNext = 'dlist-html';
1719
+ }
1720
+ return accum;
1721
+ }
1722
+ else if (!isQanda && dlistFormat === 'pandoc') {
1723
+ // Preserve ordered list context when entering dlist (push to stack instead of clearing)
1724
+ if (inList && !inList.dlist) {
1725
+ listStack.push(inList);
1726
+ inList = undefined;
1727
+ }
1728
+ // Pandoc format: term\n: description
1729
+ // All content in the definition must be indented with 4 spaces
1730
+ const pandocListIndent = indent || '';
1731
+ verbatim = verbatim?.close();
1732
+ inPara = true;
1733
+ inDlistPandoc = true;
1734
+ pandocDlistIndent = ' '; // 4 spaces for Pandoc definition content
1735
+ // Close HTML dlist if switching formats (shouldn't happen but be safe)
1736
+ if (inDlistHtml) {
1737
+ accum.push(pandocListIndent + '</dl>');
1738
+ accum.push(''); // Blank line after </dl>
1739
+ inDlistHtml = false;
1740
+ }
1741
+ // Apply attribute substitution to term and description
1742
+ const pandocTerm = attributes.call(attrs, marker);
1743
+ const pandocDesc = desc ? attributes.call(attrs, desc) : '';
1744
+ // Output term on its own line, description prefixed with :
1745
+ accum.push(pandocListIndent.trimEnd() || ''); // Blank line before term
1746
+ accum.push(pandocListIndent + pandocTerm);
1747
+ if (desc) {
1748
+ accum.push(pandocListIndent + ': ' + pandocDesc);
1749
+ pandocDlistPendingColon = false;
1750
+ }
1751
+ else {
1752
+ // Description follows on next line - defer : prefix until first content
1753
+ pandocDlistPendingColon = true;
1754
+ }
1755
+ return accum;
1756
+ }
1757
+ else {
1758
+ // Markdown format (default) or qanda
1759
+ isQanda
1760
+ ? (text = '_' + marker + '_') && (marker = '.'.repeat(dlist.length - 1)) && (dlist = 'qanda')
1761
+ : (text = '*' + marker + '*') && (marker = dlist.length > 2 ? '-'.repeat(dlist.length - 1) : '-');
1762
+ if (!(next = desc))
1763
+ hardbreakNext = 'pending';
1764
+ // Close HTML dlist if we were in one (shouldn't happen but be safe)
1765
+ if (inDlistHtml) {
1766
+ accum.push((indent || '') + '</dl>');
1767
+ accum.push(''); // Blank line after </dl>
1768
+ inDlistHtml = false;
1769
+ }
1770
+ }
1771
+ }
1772
+ const ordered = (marker[0] === '<' && !!(marker = '<.>')) || marker[0] === '.' ||
1773
+ (marker[marker.length - 1] === '.' && !!(marker = '1.'));
1774
+ if (!inList || inList.marker !== marker) {
1775
+ if ((listStack.length && ~(match = listStack.findIndex((it) => it.marker === marker)))) {
1776
+ indent = (inList = (listStack = match ? listStack.slice(0, match + 1) : [listStack[0]]).pop()).indent;
1777
+ }
1778
+ else {
1779
+ indent = (inList ? (listStack[listStack.length] = inList) : inContainer).childIndent || '';
1780
+ const lindent = (attrs.lindent ??= parseInt(attrs.get('markdown-list-indent'), 10)) || (ordered ? 3 : 2);
1781
+ inList = { marker, indent, childIndent: indent + ' '.repeat(lindent), numeral: ordered && 0, dlist };
1782
+ }
1783
+ }
1784
+ else
1785
+ indent = inList.indent;
1786
+ // List items inside a dlist definition are legitimate content — don't reset dlist state.
1787
+ // Dlist state is properly reset by: blank line (when pendingColon is false),
1788
+ // heading detection, or new dlist term re-initializing state.
1789
+ verbatim = verbatim?.close();
1790
+ subs = (inPara = true) && NORMAL_SUBS;
1791
+ line = (ordered ? '1. ' : '* ') + text;
1792
+ }
1793
+ else if (inPara) {
1794
+ subs = NORMAL_SUBS;
1795
+ if (verbatim) {
1796
+ if (indented ? undef((subs = verbatim.subs)) : !(inPara = verbatim = verbatim.close()))
1797
+ continue; // redo
1798
+ line = line.substring(verbatim.outdent);
1799
+ }
1800
+ else if (hardbreakNext === 'dlist-html') {
1801
+ // HTML definition list - append description and close dd tag
1802
+ // Apply attribute substitution to the description
1803
+ accum[accum.length - 1] += attributes.call(attrs, line) + '</dd>';
1804
+ hardbreakNext = undefined;
1805
+ return accum;
1806
+ }
1807
+ else if (hardbreakNext || inPara === 'hardbreaks') {
1808
+ accum[accum.length - 1] += attrs.get('markdown-line-break');
1809
+ }
1810
+ else if ((grab = accum[accum.length - 1])?.[grab.length - 1] === '+' && grab[grab.length - 2] === ' ') {
1811
+ accum[accum.length - 1] = hardbreak(grab, attrs.get('markdown-line-break'), true);
1812
+ }
1813
+ else if (attrs.has('markdown-unwrap-prose')) {
1814
+ ;
1815
+ (inPara !== '> ' || ((line = line.trimEnd()) !== '>' && accum[accum.length - 1] !== '>' &&
1816
+ (line = line.substring(2)))) && (indent = accum.pop() + ' ');
1817
+ }
1818
+ }
1819
+ else if (chr0 === '.') {
1820
+ subs = NORMAL_SUBS;
1821
+ if (line !== chr0 && !(line[1] === '.' && line[2] === '.') && (match = line.length - 1)) {
1822
+ const text = line[1] === '*' && line[match] === '*' ? line.substring(2, match) : line.substring(1);
1823
+ blockTitle = (line = undefined) || { indent, text, subs };
1824
+ }
1825
+ }
1826
+ else if (indented) {
1827
+ if (blockAttrs && blockAttrs.get('subs')?.includes('attributes'))
1828
+ subs = ['attributes'];
1829
+ const outdent = line.length - (line = _line).length;
1830
+ if ((inPara = true) && _chr0 === '$' && _line[1] === ' ') {
1831
+ indent = (inList || inContainer).childIndent || '';
1832
+ verbatim = { cap: indent + '```', close: () => accum.push(verbatim.cap) && undefined, outdent, subs };
1833
+ line = '```console\n' + indent + line;
1834
+ }
1835
+ else {
1836
+ indent = ((inList || inContainer).childIndent || '') + ' ';
1837
+ verbatim = { close: undef, outdent, subs };
1838
+ }
1839
+ }
1840
+ else if (~(match = line.indexOf(': ')) && match < 10 && (style = line.substring(0, match)) in ADMONS) {
1841
+ next = (inPara = true) && line.substring(match + 2);
1842
+ line = '**' + ADMONS[style] + ' ' + style + '**';
1843
+ }
1844
+ else if (blockAttrs && (style = blockAttrs.get(1)) in ADMONS) {
1845
+ // Paragraph-style admonition: [NOTE] followed by paragraph text (no ==== delimiters)
1846
+ next = (inPara = true) && line;
1847
+ line = '**' + ADMONS[style] + ' ' + style + '**';
1848
+ blockAttrs = undefined;
1849
+ }
1850
+ else if (chr0 === 'i' && line.startsWith('image::') && (match = BlockImageMacroRx.exec(line))) {
1851
+ line = image.apply(attrs, match, (subs = ['attributes']));
1852
+ }
1853
+ else if (line in BREAKS) {
1854
+ line = BREAKS[line];
1855
+ }
1856
+ else {
1857
+ // Add blank line before starting new paragraph after list continuation (+)
1858
+ // Skip when pendingColon is true — first content after term needs : prefix, not a blank line
1859
+ if (inPara === false && (inList || inDlistPandoc) && !pandocDlistPendingColon) {
1860
+ accum.push(indent.trimEnd());
1861
+ }
1862
+ inPara = blockAttrs?.has('hardbreaks-option') ? 'hardbreaks' : chr0 === '>' && line[1] === ' ' ? '> ' : true;
1863
+ if ((grab = blockAttrs?.get('id'))) {
1864
+ if (blockTitle) {
1865
+ blockTitle.id = grab;
1866
+ }
1867
+ else {
1868
+ const convertedId = attributes.call(attrs, grab);
1869
+ let anchorHtml = '<a name="' + convertedId + '"></a>';
1870
+ // Prepend pending Pandoc dlist colon to anchor
1871
+ if (pandocDlistPendingColon) {
1872
+ anchorHtml = ': ' + anchorHtml;
1873
+ pandocDlistPendingColon = false;
1874
+ }
1875
+ accum.push(anchorHtml);
1876
+ accum.push('');
1877
+ }
1878
+ blockAttrs.delete('id');
1879
+ }
1880
+ subs = NORMAL_SUBS;
1881
+ }
1882
+ }
1883
+ if (line) {
1884
+ blockTitle &&= writeBlockTitle(accum, blockTitle, blockAttrs, attrs, refs, currentHeadingLevel);
1885
+ if (subs && !(line = applySubs.call(attrs, line, subs)) && !accum.length)
1886
+ return accum;
1887
+ // Apply Pandoc definition list indentation if active
1888
+ const pdIndent = pandocDlistIndent + indent;
1889
+ let finalLine;
1890
+ // Prepend pending Pandoc dlist colon to first content
1891
+ if (pandocDlistPendingColon) {
1892
+ finalLine = indent + ': ' + line;
1893
+ pandocDlistPendingColon = false;
1894
+ }
1895
+ else {
1896
+ finalLine = pdIndent && line ? pdIndent + line : line;
1897
+ }
1898
+ accum[accum.length] = finalLine;
1899
+ (next &&= applySubs.call(attrs, next)) && (accum[accum.length - 1] += attrs.get('markdown-line-break')) &&
1900
+ (accum[accum.length] = pdIndent ? pdIndent + next : next);
1901
+ }
1902
+ else if (line === undefined) {
1903
+ return accum;
1904
+ }
1905
+ else
1906
+ accum[accum.length] = pandocDlistIndent + (indent ? indent.trimEnd() : line);
1907
+ hardbreakNext &&= hardbreakNext === 'pending' || undefined;
1908
+ return accum;
1909
+ }
1910
+ }, [])
1911
+ .concat(inDlistHtml ? [(indent || '') + '</dl>', ''] : []) // Close HTML definition list at end of document
1912
+ .join('\n')
1913
+ .trimEnd()
1914
+ // Remove standalone AsciiDoc continuation markers (+) that weren't caught during processing
1915
+ .replace(/\n\+[ \t]*\n/g, '\n\n')
1916
+ // Remove __TOPLEVEL__ markers from Nunjucks includes (prevents indentation of top-level list includes)
1917
+ .replace(/^(\s*)__TOPLEVEL__/gm, '')
1918
+ .replace(RewriteInternalXrefRx, (_, text, id) => {
1919
+ const { title = id, reftext = title, autoId = id } = refs.get(id) || nrefs.get(id) || {};
1920
+ return '[' + (text || reftext) + '](#' + autoId + ')';
1921
+ })
1922
+ .concat(((grab = verbatim?.cap)) ? '\n' + grab : ''); // prettier-ignore
1923
+ }
1924
+ function applySubs(str, subs = NORMAL_SUBS) {
1925
+ return /[{\x60\x27*_:<[#]/.test(str) ? subs.reduce((str, name) => SUBSTITUTORS[name].call(this, str), str) : str;
1926
+ }
1927
+ function attributes(str) {
1928
+ // Convert AsciiDoc attribute references {attr} to Nunjucks variables {{ attr }}
1929
+ // Escaped attributes \{attr} are kept as literal {attr}
1930
+ // Hyphens in attribute names are converted to underscores for Jinja2 compatibility
1931
+ return ~str.indexOf('{') ? str.replace(AttributeRefRx, (m, bs, n) => (bs ? m.substring(1) : '{{ ' + n.replace(/-/g, '_') + ' }}')) : str;
1932
+ }
1933
+ function callouts(str, apply = str[str.length - 1] === '>') {
1934
+ return apply ? str.replace(ConumRx, (_, sp, chr) => sp + CONUMS[chr === '.' ? this.coseq++ : chr]) : str;
1935
+ }
1936
+ function hardbreak(str, mark, force, len = str.length) {
1937
+ return force || (str[len - 1] === '+' && str[len - 2] === ' ') ? str.substring(0, str.length - 2) + mark : str;
1938
+ }
1939
+ function image(_, target, attrlist) {
1940
+ const parts = attrlist.split(',');
1941
+ const alt = parts[0].trim() || /(.*\/)?(.*?)($|\.)/.exec(target)[2];
1942
+ const width = parts[1] ? parts[1].trim() : '';
1943
+ const height = parts[2] ? parts[2].trim() : '';
1944
+ const src = (this.get('imagesdir') ? this.get('imagesdir') + '/' : '') + target;
1945
+ if (/^\d+$/.test(width) || /^\d+$/.test(height)) {
1946
+ const wAttr = /^\d+$/.test(width) ? ' width="' + width + '"' : '';
1947
+ const hAttr = /^\d+$/.test(height) ? ' height="' + height + '"' : '';
1948
+ return '<img src="' + src + '" alt="' + alt + '"' + wAttr + hAttr + '>';
1949
+ }
1950
+ return '![' + alt + '](' + src + ')';
1951
+ }
1952
+ function isAnyListItem(chr0, str, mode = 'test', match = chr0 in LIST_MARKERS && ListItemRx[mode](str)) {
1953
+ return match || (str.endsWith('::') || ~str.indexOf(':: ') ? DlistItemRx[mode](str) : undefined);
1954
+ }
1955
+ function isHeading(str, acceptAll, blockAttrs, marker, title, spaceIdx = str.indexOf(' ')) {
1956
+ if (!(~spaceIdx && str.startsWith((marker = ['=', '==', '===', '====', '=====', '======'][spaceIdx - 1]))))
1957
+ return;
1958
+ if (!(title = str.substring(spaceIdx + 1)) || (title[0] === ' ' && !(title = title.trimStart())))
1959
+ return;
1960
+ if (acceptAll || (blockAttrs && blockAttrs.get(1) === 'discrete'))
1961
+ return [marker, title];
1962
+ }
1963
+ function macros(str) {
1964
+ if (!~str.indexOf(':'))
1965
+ return ~str.indexOf('[[') ? str.replace(InlineAnchorRx, '<a name="$1"></a>') : str;
1966
+ if (~str.indexOf('m:['))
1967
+ str = str.replace(InlineStemMacroRx, (_, expr) => '$' + expr.replace(/\\]/g, ']') + '$');
1968
+ if (~str.indexOf('image:'))
1969
+ str = str.replace(InlineImageMacroRx, image.bind(this));
1970
+ if (~str.indexOf(':/') || ~str.indexOf('link:')) {
1971
+ str = str.replace(LinkMacroRx, (_, esc, scheme = '', url, boxed = '', text, bareScheme = scheme, bareUrl) => {
1972
+ if (esc)
1973
+ return bareScheme ? '<span>' + bareScheme + '</span>' + (bareUrl ?? url + boxed) : 'link:' + url + boxed;
1974
+ if (!bareUrl)
1975
+ return '[' + (text ||= this.has('hide-uri-scheme') ? url : scheme + url) + '](' + scheme + url + ')';
1976
+ return this.has('hide-uri-scheme') ? '[' + bareUrl + '](' + bareScheme + bareUrl + ')' : bareScheme + bareUrl;
1977
+ });
1978
+ }
1979
+ if (~str.indexOf('[['))
1980
+ str = str.replace(InlineAnchorRx, '<a name="$1"></a>');
1981
+ // UI macros - convert to markdown bold format
1982
+ if (~str.indexOf('btn:[')) {
1983
+ str = str.replace(/btn:\[([^\]]+)\]/g, '**[$1]**');
1984
+ }
1985
+ if (~str.indexOf('kbd:[')) {
1986
+ str = str.replace(/kbd:\[([^\]]+)\]/g, '<kbd>$1</kbd>');
1987
+ }
1988
+ if (~str.indexOf('menu:')) {
1989
+ str = str.replace(/menu:([^\[]+)\[([^\]]+)\]/g, (_, menu, path) => {
1990
+ const parts = path.split(' > ').map(s => s.trim());
1991
+ let result = menu;
1992
+ parts.forEach((part) => {
1993
+ result += ' → ' + part;
1994
+ });
1995
+ return '**' + result + '**';
1996
+ });
1997
+ }
1998
+ if (!~str.indexOf('xref:'))
1999
+ return str;
2000
+ return str.replace(XrefMacroRx, (m, esc, p, ext, id_, id = id_, txt) => esc ? m.substring(1) : '[' + (p && (ext === '#' || (p += ext)) ? txt || p : (p = '#!' + id) && txt) + '](' + p + ')');
2001
+ }
2002
+ function parseAttrlist(attrlist, attrs = new Map()) {
2003
+ if (!attrlist)
2004
+ return attrs;
2005
+ attrs.set(0, attrlist);
2006
+ let chr0, idx, m, shorthand, style;
2007
+ if ((chr0 = attrlist[0]) === '[' && (m = BlockAnchorRx.exec(attrlist))) {
2008
+ return m[2] ? attrs.set('id', m[1]).set('reftext', m[2]) : attrs.set('id', m[1]);
2009
+ }
2010
+ if (!(idx = 0) && (~attrlist.indexOf('=') || ~attrlist.indexOf('"'))) {
2011
+ while ((m = ElementAttributeRx.exec(attrlist))) {
2012
+ attrs.set(m[1] ?? ++idx, m[4] ?? m[3]);
2013
+ if (!m.index)
2014
+ attrlist = (ElementAttributeRx.lastIndex = 1) && ',' + attrlist;
2015
+ }
2016
+ }
2017
+ else if (chr0 === ',' || ~attrlist.indexOf(',')) {
2018
+ for (const it of attrlist.split(','))
2019
+ attrs.set(++idx, it.trimStart());
2020
+ }
2021
+ else
2022
+ attrs.set(1, attrlist);
2023
+ if (!(shorthand = attrs.get(1)) || (m = shorthand.split(StyleShorthandMarkersRx)).length < 2)
2024
+ return attrs;
2025
+ for (let i = 0, len = m.length, val; i < len; i += 2) {
2026
+ if ((val = m[i]) && ((chr0 = m[i - 1]) || !(style = val))) {
2027
+ chr0 === '#' ? attrs.set('id', val) : chr0 === '.' ? attrs.set('role', val) : attrs.set(val + '-option', '');
2028
+ }
2029
+ }
2030
+ return attrs.set(1, style);
2031
+ }
2032
+ function quotes(str, idx) {
2033
+ const hasLt = ~(~str.indexOf('<<') ? (str = str.replace(XrefShorthandRx, 'xref:$1[$2]')) : str).indexOf('<');
2034
+ if (hasLt)
2035
+ str = str.replace(/</g, '&lt;');
2036
+ if (~(idx = str.indexOf('*')) && ~str.indexOf('*', idx + 1))
2037
+ str = str.replace(StrongSpanRx, '*$1*');
2038
+ if (~str.indexOf('`') && ((idx = ~str.indexOf('"`') || ~str.indexOf("'`")) || true)) {
2039
+ if (idx)
2040
+ str = str.replace(QuotedSpanRx, (this.q ??= this.get('quotes').split(' ').slice(0, 2).join('$2')));
2041
+ if (hasLt || ~str.indexOf('`+') || ~str.indexOf(']`') || ~str.indexOf('\\')) {
2042
+ str = str.replace(/(?:\[[^[\]]+\])?`(\+)?(\S|\S.*?\S)\1`/g, (_, pass, text) => {
2043
+ if (hasLt && text.length > 3 && ~text.indexOf('&lt;'))
2044
+ text = text.replace(/&lt;/g, '<');
2045
+ if (pass)
2046
+ return '`' + (~text.indexOf('{') ? text.replace(/\{(?=[a-z])/g, '\\{') : text) + '`';
2047
+ return '`' + (~text.indexOf('\\') ? text.replace(/\\(?=https?:|\.\.\.)/g, '') : text) + '`';
2048
+ });
2049
+ }
2050
+ }
2051
+ if (~str.indexOf(']_'))
2052
+ str = str.replace(EmphasisSpanMetaRx, '');
2053
+ if (~(idx = str.indexOf('#')) && ~str.indexOf('#', idx + 1)) {
2054
+ str = str.replace(MarkedSpanRx, (_, roles, s, text) => {
2055
+ s &&= this.s ??= (s = this.get('markdown-strikethrough').split(' ')).length > 1 ? s.slice(0, 2) : [s[0], s[0]];
2056
+ return roles ? (s ? s[0] + text + s[1] : text) : '<mark>' + text + '</mark>';
2057
+ });
2058
+ }
2059
+ if (!~str.indexOf("'"))
2060
+ return str;
2061
+ return str.replace(/.'/g, (m, i, s, l = m[0], r = s[i + 2] || '') => l === '`' ? (r === '`' ? m : '\u2019') : /\p{L}/u.test(r) && /[\p{L}\d]/u.test(l) ? l + '\u2019' : m);
2062
+ }
2063
+ function writeBlockTitle(buffer, blockTitle, blockAttrs, attrs, refs, headingLevel = 2) {
2064
+ const { id = blockAttrs?.get('id'), indent, text, subs, title = applySubs.call(attrs, text, subs) } = blockTitle;
2065
+ // Convert section titles to headings one level deeper than current context
2066
+ const level = Math.min(6, (headingLevel || 1) + 1); // Default to H2, max H6
2067
+ const heading = '#'.repeat(level) + ' ' + title;
2068
+ // Convert attribute references in ID to Jinja2 format (e.g., {context} → {{ context }})
2069
+ const convertedId = id ? attributes.call(attrs, id) : null;
2070
+ const idSuffix = convertedId ? ` {id="${convertedId}"}` : '';
2071
+ if (id)
2072
+ refs.set(id, { title, reftext: blockAttrs?.get('reftext') });
2073
+ buffer.push(indent + heading + idSuffix, '');
2074
+ }
2075
+ //# sourceMappingURL=engine.js.map