@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/LINEAGE.md +67 -0
- package/README.md +38 -0
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +2075 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
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":" ","quotes":"<q> </q>","sp":" ","vbar":"|","zwsp":"​"}');
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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, ' ') + '</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 '';
|
|
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, '<');
|
|
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('<'))
|
|
2044
|
+
text = text.replace(/</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
|