@asciidoctor/core 3.0.4 → 4.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +42 -10
  2. package/build/browser/index.js +24154 -0
  3. package/build/node/index.cjs +24735 -0
  4. package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
  5. package/package.json +53 -100
  6. package/src/abstract_block.js +849 -0
  7. package/src/abstract_node.js +954 -0
  8. package/src/attribute_entry.js +12 -0
  9. package/src/attribute_list.js +380 -0
  10. package/src/block.js +168 -0
  11. package/src/browser/asset.js +22 -0
  12. package/src/browser/reader.js +138 -0
  13. package/src/browser.js +121 -0
  14. package/src/callouts.js +85 -0
  15. package/src/compliance.js +54 -0
  16. package/src/constants.js +665 -0
  17. package/src/convert.js +370 -0
  18. package/src/converter/composite.js +83 -0
  19. package/src/converter/docbook5.js +1031 -0
  20. package/src/converter/html5.js +1899 -0
  21. package/src/converter/manpage.js +935 -0
  22. package/src/converter/template.js +459 -0
  23. package/src/converter.js +478 -0
  24. package/src/data/stylesheet-data.js +2 -0
  25. package/src/document.js +2134 -0
  26. package/src/extensions.js +1952 -0
  27. package/src/footnote.js +28 -0
  28. package/src/helpers.js +355 -0
  29. package/src/index.js +138 -0
  30. package/src/inline.js +158 -0
  31. package/src/list.js +240 -0
  32. package/src/load.js +276 -0
  33. package/src/logging.js +526 -0
  34. package/src/parser.js +3661 -0
  35. package/src/path_resolver.js +472 -0
  36. package/src/reader.js +1755 -0
  37. package/src/rx.js +829 -0
  38. package/src/section.js +354 -0
  39. package/src/stylesheets.js +30 -0
  40. package/src/substitutors.js +2241 -0
  41. package/src/syntaxHighlighter/highlightjs.js +90 -0
  42. package/src/syntaxHighlighter/html_pipeline.js +33 -0
  43. package/src/syntax_highlighter.js +304 -0
  44. package/src/table.js +952 -0
  45. package/src/timings.js +78 -0
  46. package/types/abstract_block.d.ts +343 -0
  47. package/types/abstract_node.d.ts +471 -0
  48. package/types/attribute_entry.d.ts +7 -0
  49. package/types/attribute_list.d.ts +52 -0
  50. package/types/block.d.ts +55 -0
  51. package/types/browser/asset.d.ts +7 -0
  52. package/types/browser/reader.d.ts +29 -0
  53. package/types/callouts.d.ts +36 -0
  54. package/types/compliance.d.ts +23 -0
  55. package/types/constants.d.ts +268 -0
  56. package/types/convert.d.ts +34 -0
  57. package/types/converter/composite.d.ts +20 -0
  58. package/types/converter/docbook5.d.ts +41 -0
  59. package/types/converter/html5.d.ts +51 -0
  60. package/types/converter/manpage.d.ts +59 -0
  61. package/types/converter/template.d.ts +83 -0
  62. package/types/converter.d.ts +150 -0
  63. package/types/data/stylesheet-data.d.ts +2 -0
  64. package/types/document.d.ts +495 -0
  65. package/types/extensions.d.ts +876 -0
  66. package/types/footnote.d.ts +18 -0
  67. package/types/helpers.d.ts +146 -0
  68. package/types/index.d.ts +73 -3731
  69. package/types/inline.d.ts +69 -0
  70. package/types/list.d.ts +114 -0
  71. package/types/load.d.ts +39 -0
  72. package/types/logging.d.ts +187 -0
  73. package/types/parser.d.ts +114 -0
  74. package/types/path_resolver.d.ts +103 -0
  75. package/types/reader.d.ts +184 -0
  76. package/types/rx.d.ts +513 -0
  77. package/types/section.d.ts +122 -0
  78. package/types/stylesheets.d.ts +10 -0
  79. package/types/substitutors.d.ts +208 -0
  80. package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
  81. package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
  82. package/types/syntax_highlighter.d.ts +167 -0
  83. package/types/table.d.ts +231 -0
  84. package/types/timings.d.ts +25 -0
  85. package/types/tsconfig.json +9 -0
  86. package/LICENSE +0 -21
  87. package/dist/browser/asciidoctor.js +0 -47654
  88. package/dist/browser/asciidoctor.min.js +0 -1452
  89. package/dist/graalvm/asciidoctor.js +0 -47402
  90. package/dist/node/asciidoctor.cjs +0 -21567
  91. package/dist/node/asciidoctor.js +0 -23037
@@ -0,0 +1,1899 @@
1
+ // ESM port of converter/html5.rb
2
+ //
3
+ // Ruby-to-JavaScript notes:
4
+ // - @xml_mode / @void_element_slash → this._xmlMode / this._voidSlash
5
+ // - Ruby symbol keys in QUOTE_TAGS → plain string keys
6
+ // - node.attr? → node.hasAttribute()
7
+ // - node.option? → node.hasOption()
8
+ // - node.title? → node.hasTitle()
9
+ // - node.sections? → node.hasSections()
10
+ // - node.blocks? → node.hasBlocks()
11
+ // - node.footnotes? → node.hasFootnotes()
12
+ // - node.noheader/notitle/nofooter → node.isNoheader()/isNotitle()/isNofooter()
13
+ // - node.sections → node.sections() (method)
14
+ // - await node.content() → await node.content() (method on Block/Document)
15
+ // - alias convert_pass content_only → convert_pass delegates to this.contentOnly()
16
+ // - Stylesheets.instance.primary_stylesheet_data → Stylesheets.instance.primaryStylesheetData() (camelCase, async)
17
+ // - read_svg_contents uses readContents (supports local and remote URIs via allow-uri-read)
18
+
19
+ import { ConverterBase } from '../converter.js'
20
+ import { AbstractNode } from '../abstract_node.js'
21
+ import {
22
+ LF,
23
+ SafeMode,
24
+ DEFAULT_STYLESHEET_KEYS,
25
+ DEFAULT_STYLESHEET_NAME,
26
+ FONT_AWESOME_VERSION,
27
+ MATHJAX_VERSION,
28
+ BLOCK_MATH_DELIMITERS,
29
+ INLINE_MATH_DELIMITERS,
30
+ } from '../constants.js'
31
+ import { XmlSanitizeRx } from '../rx.js'
32
+ import { extname, isUriish } from '../helpers.js'
33
+ import { Stylesheets } from '../stylesheets.js'
34
+
35
+ // ── Local regex constants ─────────────────────────────────────────────────────
36
+
37
+ const DropAnchorRx = /<(?:a\b[^>]*|\/a)>/g
38
+ const LeadingAnchorsRx = /^(?:<a id="[^"]+"><\/a>)+/
39
+ const StemBreakRx = / *\\\n(?:\\?\n)*|\n\n+/g
40
+ // NOTE In JavaScript ^ matches start of string when the m flag is not set (same as Opal)
41
+ const SvgPreambleRx = /^[\s\S]*?(?=<svg[\s>])/
42
+ const SvgStartTagRx = /^<svg(?:\s[^>]*)?>/
43
+ const DimensionAttributeRx = /\s(?:width|height|style)=(["'])[\s\S]*?\1/g
44
+
45
+ // ── Quote tag table ───────────────────────────────────────────────────────────
46
+
47
+ const QUOTE_TAGS = {
48
+ monospaced: ['<code>', '</code>', true],
49
+ emphasis: ['<em>', '</em>', true],
50
+ strong: ['<strong>', '</strong>', true],
51
+ double: ['&#8220;', '&#8221;'],
52
+ single: ['&#8216;', '&#8217;'],
53
+ mark: ['<mark>', '</mark>', true],
54
+ superscript: ['<sup>', '</sup>', true],
55
+ subscript: ['<sub>', '</sub>', true],
56
+ asciimath: ['\\$', '\\$'],
57
+ latexmath: ['\\(', '\\)'],
58
+ }
59
+ const DEFAULT_QUOTE_TAG = ['', '']
60
+
61
+ // ── Html5Converter ────────────────────────────────────────────────────────────
62
+
63
+ export default class Html5Converter extends ConverterBase {
64
+ /**
65
+ * Create a new Html5Converter instance.
66
+ * @param {string} [backend='html5']
67
+ * @param {Object} [opts={}]
68
+ * @returns {Html5Converter}
69
+ */
70
+ static create(backend = 'html5', opts = {}) {
71
+ return new this(backend, opts)
72
+ }
73
+
74
+ constructor(backend, opts = {}) {
75
+ super(backend, opts)
76
+ let syntax
77
+ if (opts.htmlsyntax === 'xml') {
78
+ syntax = 'xml'
79
+ this._xmlMode = true
80
+ this._voidSlash = '/'
81
+ } else {
82
+ syntax = 'html'
83
+ this._xmlMode = false
84
+ this._voidSlash = ''
85
+ }
86
+ this.initBackendTraits({
87
+ basebackend: 'html',
88
+ filetype: 'html',
89
+ htmlsyntax: syntax,
90
+ outfilesuffix: '.html',
91
+ supportsTemplates: true,
92
+ })
93
+ }
94
+
95
+ async convert_document(node) {
96
+ const slash = this._voidSlash
97
+ const br = `<br${slash}>`
98
+ let assetUriScheme = node.getAttribute('asset-uri-scheme', 'https')
99
+ if (assetUriScheme) assetUriScheme = `${assetUriScheme}:`
100
+ const cdnBaseUrl = `${assetUriScheme}//cdnjs.cloudflare.com/ajax/libs`
101
+ const linkcss = node.hasAttribute('linkcss')
102
+ const maxWidthAttr = node.hasAttribute('max-width')
103
+ ? ` style="max-width: ${node.getAttribute('max-width')};"`
104
+ : ''
105
+ const result = ['<!DOCTYPE html>']
106
+ const langAttribute = node.hasAttribute('nolang')
107
+ ? ''
108
+ : ` lang="${node.getAttribute('lang', 'en')}"`
109
+ result.push(
110
+ `<html${this._xmlMode ? ' xmlns="http://www.w3.org/1999/xhtml"' : ''}${langAttribute}>`
111
+ )
112
+ result.push(`<head>
113
+ <meta charset="${node.getAttribute('encoding', 'UTF-8')}"${slash}>
114
+ <meta http-equiv="X-UA-Compatible" content="IE=edge"${slash}>
115
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"${slash}>`)
116
+ let reproducible
117
+ if (!(reproducible = node.hasAttribute('reproducible'))) {
118
+ result.push(
119
+ `<meta name="generator" content="Asciidoctor ${node.getAttribute('asciidoctor-version')}"${slash}>`
120
+ )
121
+ }
122
+ if (node.hasAttribute('app-name')) {
123
+ result.push(
124
+ `<meta name="application-name" content="${node.getAttribute('app-name')}"${slash}>`
125
+ )
126
+ }
127
+ if (node.hasAttribute('description')) {
128
+ result.push(
129
+ `<meta name="description" content="${node.getAttribute('description')}"${slash}>`
130
+ )
131
+ }
132
+ if (node.hasAttribute('keywords')) {
133
+ result.push(
134
+ `<meta name="keywords" content="${node.getAttribute('keywords')}"${slash}>`
135
+ )
136
+ }
137
+ if (node.hasAttribute('authors')) {
138
+ let authors = node.subReplacements(node.getAttribute('authors'))
139
+ if (authors.includes('<')) authors = authors.replace(XmlSanitizeRx, '')
140
+ result.push(`<meta name="author" content="${authors}"${slash}>`)
141
+ }
142
+ if (node.hasAttribute('copyright')) {
143
+ result.push(
144
+ `<meta name="copyright" content="${node.getAttribute('copyright')}"${slash}>`
145
+ )
146
+ }
147
+ if (node.hasAttribute('favicon')) {
148
+ // Access raw attribute value to detect empty string (set without value)
149
+ let iconHref = 'favicon' in node.attributes ? node.attributes.favicon : ''
150
+ let iconType
151
+ if (!iconHref) {
152
+ iconHref = 'favicon.ico'
153
+ iconType = 'image/x-icon'
154
+ } else {
155
+ const iconExt = extname(iconHref, null)
156
+ if (iconExt) {
157
+ iconType =
158
+ iconExt === '.ico' ? 'image/x-icon' : `image/${iconExt.slice(1)}`
159
+ } else {
160
+ iconType = 'image/x-icon'
161
+ }
162
+ }
163
+ result.push(
164
+ `<link rel="icon" type="${iconType}" href="${iconHref}"${slash}>`
165
+ )
166
+ }
167
+ result.push(
168
+ `<title>${node.doctitle({ sanitize: true, use_fallback: true })}</title>`
169
+ )
170
+
171
+ // Access raw attribute value; '' means "use default stylesheet"
172
+ const stylesheetRawVal =
173
+ 'stylesheet' in node.attributes ? node.attributes.stylesheet : null
174
+ if (DEFAULT_STYLESHEET_KEYS.has(stylesheetRawVal)) {
175
+ if (node.hasAttribute('webfonts')) {
176
+ const webfonts = node.attributes.webfonts ?? ''
177
+ const fontFamily =
178
+ webfonts ||
179
+ 'Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CNoto+Sans+Mono:400,700'
180
+ result.push(
181
+ `<link rel="stylesheet" href="${assetUriScheme}//fonts.googleapis.com/css?family=${fontFamily}"${slash}>`
182
+ )
183
+ }
184
+ if (linkcss) {
185
+ result.push(
186
+ `<link rel="stylesheet" href="${node.normalizeWebPath(DEFAULT_STYLESHEET_NAME, node.getAttribute('stylesdir'), false)}"${slash}>`
187
+ )
188
+ } else {
189
+ result.push(
190
+ `<style>\n${await Stylesheets.instance.primaryStylesheetData()}\n</style>`
191
+ )
192
+ }
193
+ } else if (node.hasAttribute('stylesheet')) {
194
+ if (linkcss) {
195
+ result.push(
196
+ `<link rel="stylesheet" href="${node.normalizeWebPath(node.getAttribute('stylesheet'), node.getAttribute('stylesdir'))}"${slash}>`
197
+ )
198
+ } else {
199
+ const cssPath = node.normalizeSystemPath(
200
+ node.getAttribute('stylesheet'),
201
+ node.getAttribute('stylesdir')
202
+ )
203
+ const cssData =
204
+ (await node.readAsset(cssPath, {
205
+ warnOnFailure: true,
206
+ label: 'stylesheet',
207
+ })) ?? ''
208
+ result.push(`<style>\n${cssData}\n</style>`)
209
+ }
210
+ }
211
+
212
+ if (node.hasAttribute('icons', 'font')) {
213
+ if (node.hasAttribute('iconfont-remote')) {
214
+ const cdnUrl =
215
+ node.getAttribute('iconfont-cdn') ??
216
+ `${cdnBaseUrl}/font-awesome/${FONT_AWESOME_VERSION}/css/font-awesome.min.css`
217
+ result.push(`<link rel="stylesheet" href="${cdnUrl}"${slash}>`)
218
+ } else {
219
+ const iconfontStylesheet = `${node.getAttribute('iconfont-name', 'font-awesome')}.css`
220
+ result.push(
221
+ `<link rel="stylesheet" href="${node.normalizeWebPath(iconfontStylesheet, node.getAttribute('stylesdir'), false)}"${slash}>`
222
+ )
223
+ }
224
+ }
225
+
226
+ const syntaxHl = node.syntaxHighlighter
227
+ let syntaxHlDocinfoHeadIdx
228
+ if (syntaxHl) {
229
+ syntaxHlDocinfoHeadIdx = result.length
230
+ result.push('') // placeholder; replaced or spliced out below
231
+ }
232
+
233
+ const docinfoContent = await node.docinfo()
234
+ if (docinfoContent) result.push(docinfoContent)
235
+
236
+ result.push('</head>')
237
+
238
+ const idAttr = node.id ? ` id="${node.id}"` : ''
239
+ const sectioned = node.hasSections()
240
+ let classes
241
+ if (
242
+ sectioned &&
243
+ node.hasAttribute('toc-class') &&
244
+ node.hasAttribute('toc') &&
245
+ node.hasAttribute('toc-placement', 'auto')
246
+ ) {
247
+ classes = [
248
+ node.doctype,
249
+ node.getAttribute('toc-class'),
250
+ `toc-${node.getAttribute('toc-position', 'header')}`,
251
+ ]
252
+ } else {
253
+ classes = [node.doctype]
254
+ }
255
+ if (node.role) classes.push(node.role)
256
+ result.push(`<body${idAttr} class="${classes.join(' ')}">`)
257
+
258
+ const headerDocinfo = await node.docinfo('header')
259
+ if (headerDocinfo) result.push(headerDocinfo)
260
+
261
+ if (!node.isNoheader()) {
262
+ result.push(`<div id="header"${maxWidthAttr}>`)
263
+ if (node.doctype === 'manpage') {
264
+ result.push(`<h1>${node.doctitle()} Manual Page</h1>`)
265
+ if (
266
+ sectioned &&
267
+ node.hasAttribute('toc') &&
268
+ node.hasAttribute('toc-placement', 'auto')
269
+ ) {
270
+ result.push(`<div id="toc" class="${node.getAttribute('toc-class', 'toc')}">
271
+ <div id="toctitle">${node.getAttribute('toc-title')}</div>
272
+ ${await node.converter.convert(node, 'outline')}
273
+ </div>`)
274
+ }
275
+ if (node.hasAttribute('manpurpose'))
276
+ result.push(this._generateMannameSection(node))
277
+ } else {
278
+ if (node.hasHeader()) {
279
+ if (!node.isNotitle()) result.push(`<h1>${node.header.title}</h1>`)
280
+ const details = []
281
+ let idx = 1
282
+ for (const author of node.authors()) {
283
+ details.push(
284
+ `<span id="author${idx > 1 ? idx : ''}" class="author">${node.subReplacements(author.name)}</span>${br}`
285
+ )
286
+ if (author.email) {
287
+ details.push(
288
+ `<span id="email${idx > 1 ? idx : ''}" class="email">${await node.subMacros(author.email)}</span>${br}`
289
+ )
290
+ }
291
+ idx++
292
+ }
293
+ if (node.hasAttribute('revnumber')) {
294
+ const versionLabel = (
295
+ node.getAttribute('version-label') || ''
296
+ ).toLowerCase()
297
+ details.push(
298
+ `<span id="revnumber">${versionLabel} ${node.getAttribute('revnumber')}${node.hasAttribute('revdate') ? ',' : ''}</span>`
299
+ )
300
+ }
301
+ if (node.hasAttribute('revdate')) {
302
+ details.push(
303
+ `<span id="revdate">${node.getAttribute('revdate')}</span>`
304
+ )
305
+ }
306
+ if (node.hasAttribute('revremark')) {
307
+ details.push(
308
+ `${br}<span id="revremark">${node.getAttribute('revremark')}</span>`
309
+ )
310
+ }
311
+ if (details.length > 0) {
312
+ result.push('<div class="details">')
313
+ result.push(...details)
314
+ result.push('</div>')
315
+ }
316
+ }
317
+ if (
318
+ sectioned &&
319
+ node.hasAttribute('toc') &&
320
+ node.hasAttribute('toc-placement', 'auto')
321
+ ) {
322
+ result.push(`<div id="toc" class="${node.getAttribute('toc-class', 'toc')}">
323
+ <div id="toctitle">${node.getAttribute('toc-title')}</div>
324
+ ${await node.converter.convert(node, 'outline')}
325
+ </div>`)
326
+ }
327
+ }
328
+ result.push('</div>')
329
+ }
330
+
331
+ result.push(`<div id="content"${maxWidthAttr}>
332
+ ${await node.content()}
333
+ </div>`)
334
+
335
+ if (node.hasFootnotes() && !node.hasAttribute('nofootnotes')) {
336
+ result.push(`<div id="footnotes"${maxWidthAttr}>
337
+ <hr${slash}>`)
338
+ for (const footnote of node.footnotes) {
339
+ result.push(`<div class="footnote" id="_footnotedef_${footnote.index}">
340
+ <a href="#_footnoteref_${footnote.index}">${footnote.index}</a>. ${footnote.text}
341
+ </div>`)
342
+ }
343
+ result.push('</div>')
344
+ }
345
+
346
+ if (!node.isNofooter()) {
347
+ result.push(`<div id="footer"${maxWidthAttr}>`)
348
+ result.push('<div id="footer-text">')
349
+ if (node.hasAttribute('revnumber')) {
350
+ result.push(
351
+ `${node.getAttribute('version-label')} ${node.getAttribute('revnumber')}${br}`
352
+ )
353
+ }
354
+ if (node.hasAttribute('last-update-label') && !reproducible) {
355
+ result.push(
356
+ `${node.getAttribute('last-update-label')} ${node.getAttribute('docdatetime')}`
357
+ )
358
+ }
359
+ result.push('</div>')
360
+ result.push('</div>')
361
+ }
362
+
363
+ // JavaScript (and auxiliary stylesheets) loaded at end of body for performance
364
+ if (syntaxHl) {
365
+ if (syntaxHl.hasDocinfo('head')) {
366
+ result[syntaxHlDocinfoHeadIdx] = syntaxHl.docinfo('head', node, {
367
+ cdn_base_url: cdnBaseUrl,
368
+ linkcss,
369
+ self_closing_tag_slash: slash,
370
+ })
371
+ } else {
372
+ result.splice(syntaxHlDocinfoHeadIdx, 1)
373
+ }
374
+ if (syntaxHl.hasDocinfo('footer')) {
375
+ result.push(
376
+ syntaxHl.docinfo('footer', node, {
377
+ cdn_base_url: cdnBaseUrl,
378
+ linkcss,
379
+ self_closing_tag_slash: slash,
380
+ })
381
+ )
382
+ }
383
+ }
384
+
385
+ if (node.hasAttribute('stem')) {
386
+ let eqnumsVal = node.getAttribute('eqnums', 'none')
387
+ if (!eqnumsVal) eqnumsVal = 'AMS'
388
+ const eqnumsOpt = ` equationNumbers: { autoNumber: "${eqnumsVal}" } `
389
+ // IMPORTANT inspect calls on delimiter arrays are intentional for JavaScript compat (emulates JSON.stringify)
390
+ result.push(`<script type="text/x-mathjax-config">
391
+ MathJax.Hub.Config({
392
+ messageStyle: "none",
393
+ tex2jax: {
394
+ inlineMath: [${JSON.stringify(INLINE_MATH_DELIMITERS.latexmath)}],
395
+ displayMath: [${JSON.stringify(BLOCK_MATH_DELIMITERS.latexmath)}],
396
+ ignoreClass: "nostem|nolatexmath"
397
+ },
398
+ asciimath2jax: {
399
+ delimiters: [${JSON.stringify(BLOCK_MATH_DELIMITERS.asciimath)}],
400
+ ignoreClass: "nostem|noasciimath"
401
+ },
402
+ TeX: {${eqnumsOpt}}
403
+ })
404
+ MathJax.Hub.Register.StartupHook("AsciiMath Jax Ready", function () {
405
+ MathJax.InputJax.AsciiMath.postfilterHooks.Add(function (data, node) {
406
+ if ((node = data.script.parentNode) && (node = node.parentNode) && node.classList.contains("stemblock")) {
407
+ data.math.root.display = "block"
408
+ }
409
+ return data
410
+ })
411
+ })
412
+ </script>
413
+ <script src="${cdnBaseUrl}/mathjax/${MATHJAX_VERSION}/MathJax.js?config=TeX-MML-AM_CHTML"></script>`)
414
+ }
415
+
416
+ const footerDocinfo = await node.docinfo('footer')
417
+ if (footerDocinfo) result.push(footerDocinfo)
418
+
419
+ result.push('</body>')
420
+ result.push('</html>')
421
+ return result.join(LF)
422
+ }
423
+
424
+ async convert_embedded(node) {
425
+ const result = []
426
+ if (node.doctype === 'manpage') {
427
+ if (!node.isNotitle()) {
428
+ const idAttr = node.id ? ` id="${node.id}"` : ''
429
+ result.push(`<h1${idAttr}>${node.doctitle()} Manual Page</h1>`)
430
+ }
431
+ if (node.hasAttribute('manpurpose'))
432
+ result.push(this._generateMannameSection(node))
433
+ } else if (node.hasHeader() && !node.isNotitle()) {
434
+ const idAttr = node.id ? ` id="${node.id}"` : ''
435
+ result.push(`<h1${idAttr}>${node.header.title}</h1>`)
436
+ }
437
+
438
+ if (node.hasSections() && node.hasAttribute('toc')) {
439
+ const tocP = node.getAttribute('toc-placement')
440
+ if (tocP !== 'macro' && tocP !== 'preamble') {
441
+ result.push(`<div id="toc" class="toc">
442
+ <div id="toctitle">${node.getAttribute('toc-title')}</div>
443
+ ${await node.converter.convert(node, 'outline')}
444
+ </div>`)
445
+ }
446
+ }
447
+
448
+ result.push(await node.content())
449
+
450
+ if (node.hasFootnotes() && !node.hasAttribute('nofootnotes')) {
451
+ result.push(`<div id="footnotes">
452
+ <hr${this._voidSlash}>`)
453
+ for (const footnote of node.footnotes) {
454
+ result.push(`<div class="footnote" id="_footnotedef_${footnote.index}">
455
+ <a href="#_footnoteref_${footnote.index}">${footnote.index}</a>. ${footnote.text}
456
+ </div>`)
457
+ }
458
+ result.push('</div>')
459
+ }
460
+
461
+ return result.join(LF)
462
+ }
463
+
464
+ async convert_outline(node, opts = {}) {
465
+ if (!node.hasSections()) return null
466
+ const sections = node.sections()
467
+ const parts = node.context === 'document' && node.isMultipart()
468
+ const sectlevel = parts ? 0 : sections[0].level
469
+ const sectnumlevels =
470
+ opts.sectnumlevels ??
471
+ parseInt(node.document.attributes.sectnumlevels || 3, 10)
472
+
473
+ let toclevels = opts.toclevels ?? null
474
+ if (toclevels == null) {
475
+ const toclevelAttr = node.document.attributes.toclevels
476
+ if (toclevelAttr) {
477
+ toclevels = parseInt(toclevelAttr, 10)
478
+ if (toclevels < 1 && !parts) toclevels = 1
479
+ } else {
480
+ toclevels = 2
481
+ }
482
+ }
483
+
484
+ const result = [`<ul class="sectlevel${sectlevel}">`]
485
+ for (const section of sections) {
486
+ const slevel = section.level
487
+ const stoclevels = section.hasAttribute('toclevels')
488
+ ? parseInt(section.getAttribute('toclevels'), 10)
489
+ : toclevels
490
+ if (slevel > stoclevels) continue
491
+
492
+ let stitle
493
+ if (section.caption) {
494
+ stitle = section.captionedTitle()
495
+ } else if (section.numbered && slevel <= sectnumlevels) {
496
+ if (slevel < 2 && node.document.doctype === 'book') {
497
+ const sectname = section.sectname
498
+ if (sectname === 'chapter') {
499
+ const signifier = node.document.attributes['chapter-signifier']
500
+ stitle = `${signifier ? `${signifier} ` : ''}${section.sectnum()} ${section.title}`
501
+ } else if (sectname === 'part') {
502
+ const signifier = node.document.attributes['part-signifier']
503
+ stitle = `${signifier ? `${signifier} ` : ''}${section.sectnum(null, ':')} ${section.title}`
504
+ } else {
505
+ stitle = `${section.sectnum()} ${section.title}`
506
+ }
507
+ } else {
508
+ stitle = `${section.sectnum()} ${section.title}`
509
+ }
510
+ } else {
511
+ stitle = section.title
512
+ }
513
+
514
+ if (stitle?.includes('<a')) {
515
+ stitle = stitle.replace(new RegExp(DropAnchorRx.source, 'g'), '')
516
+ }
517
+
518
+ const otag =
519
+ slevel === sectlevel ? '<li>' : `<li class="sectlevel${slevel}">`
520
+ if (slevel < stoclevels) {
521
+ const childTocLevel = await this.convert_outline(section, {
522
+ toclevels: stoclevels,
523
+ sectnumlevels,
524
+ })
525
+ if (childTocLevel) {
526
+ result.push(`${otag}<a href="#${section.id}">${stitle}</a>`)
527
+ result.push(childTocLevel)
528
+ result.push('</li>')
529
+ continue
530
+ }
531
+ }
532
+ result.push(`${otag}<a href="#${section.id}">${stitle}</a></li>`)
533
+ }
534
+ result.push('</ul>')
535
+ return result.join(LF)
536
+ }
537
+
538
+ async convert_section(node) {
539
+ const docAttrs = node.document.attributes
540
+ const level = node.level
541
+ let title
542
+ if (node.caption) {
543
+ title = node.captionedTitle()
544
+ } else if (
545
+ node.numbered &&
546
+ level <= parseInt(docAttrs.sectnumlevels || 3, 10)
547
+ ) {
548
+ if (level < 2 && node.document.doctype === 'book') {
549
+ const sectname = node.sectname
550
+ if (sectname === 'chapter') {
551
+ const signifier = docAttrs['chapter-signifier']
552
+ title = `${signifier ? `${signifier} ` : ''}${node.sectnum()} ${node.title}`
553
+ } else if (sectname === 'part') {
554
+ const signifier = docAttrs['part-signifier']
555
+ title = `${signifier ? `${signifier} ` : ''}${node.sectnum(null, ':')} ${node.title}`
556
+ } else {
557
+ title = `${node.sectnum()} ${node.title}`
558
+ }
559
+ } else {
560
+ title = `${node.sectnum()} ${node.title}`
561
+ }
562
+ } else {
563
+ title = node.title
564
+ }
565
+
566
+ let idAttr = ''
567
+ if (node.id) {
568
+ const id = node.id
569
+ idAttr = ` id="${id}"`
570
+ if ('sectlinks' in docAttrs) {
571
+ let m
572
+ if (title.startsWith('<a ') && (m = title.match(LeadingAnchorsRx))) {
573
+ title = `${m[0]}<a class="link" href="#${id}">${title.slice(m[0].length)}</a>`
574
+ } else {
575
+ title = `<a class="link" href="#${id}">${title}</a>`
576
+ }
577
+ }
578
+ if ('sectanchors' in docAttrs) {
579
+ if (docAttrs.sectanchors === 'after') {
580
+ title = `${title}<a class="anchor" href="#${id}"></a>`
581
+ } else {
582
+ title = `<a class="anchor" href="#${id}"></a>${title}`
583
+ }
584
+ }
585
+ }
586
+
587
+ const role = node.role
588
+ if (level === 0) {
589
+ return `<h1${idAttr} class="sect0${role ? ` ${role}` : ''}">${title}</h1>
590
+ ${await node.content()}`
591
+ }
592
+ return `<div class="sect${level}${role ? ` ${role}` : ''}">
593
+ <h${level + 1}${idAttr}>${title}</h${level + 1}>
594
+ ${
595
+ level === 1
596
+ ? `<div class="sectionbody">
597
+ ${await node.content()}
598
+ </div>`
599
+ : await node.content()
600
+ }
601
+ </div>`
602
+ }
603
+
604
+ async convert_admonition(node) {
605
+ const idAttr = node.id ? ` id="${node.id}"` : ''
606
+ const name = node.getAttribute('name')
607
+ const titleElement = node.hasTitle()
608
+ ? `<div class="title">${node.title}</div>\n`
609
+ : ''
610
+ let label
611
+ if (node.document.hasAttribute('icons')) {
612
+ if (
613
+ node.document.hasAttribute('icons', 'font') &&
614
+ !node.hasAttribute('icon')
615
+ ) {
616
+ label = `<i class="fa icon-${name}" title="${node.getAttribute('textlabel')}"></i>`
617
+ } else {
618
+ label = `<img src="${await node.iconUri(name)}" alt="${node.getAttribute('textlabel')}"${this._voidSlash}>`
619
+ }
620
+ } else {
621
+ label = `<div class="title">${node.getAttribute('textlabel')}</div>`
622
+ }
623
+ return `<div${idAttr} class="admonitionblock ${name}${node.role ? ` ${node.role}` : ''}">
624
+ <table>
625
+ <tr>
626
+ <td class="icon">
627
+ ${label}
628
+ </td>
629
+ <td class="content">
630
+ ${titleElement}${await node.content()}
631
+ </td>
632
+ </tr>
633
+ </table>
634
+ </div>`
635
+ }
636
+
637
+ async convert_audio(node) {
638
+ const xml = this._xmlMode
639
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
640
+ const classes = ['audioblock', node.role].filter(Boolean)
641
+ const classAttribute = ` class="${classes.join(' ')}"`
642
+ const titleElement = node.hasTitle()
643
+ ? `<div class="title">${node.title}</div>\n`
644
+ : ''
645
+ const startT = node.getAttribute('start')
646
+ const endT = node.getAttribute('end')
647
+ const timeAnchor =
648
+ startT || endT ? `#t=${startT || ''}${endT ? `,${endT}` : ''}` : ''
649
+ return `<div${idAttribute}${classAttribute}>
650
+ ${titleElement}<div class="content">
651
+ <audio src="${node.mediaUri(node.getAttribute('target'))}${timeAnchor}"${node.hasOption('autoplay') ? this._appendBooleanAttr('autoplay', xml) : ''}${node.hasOption('nocontrols') ? '' : this._appendBooleanAttr('controls', xml)}${node.hasOption('loop') ? this._appendBooleanAttr('loop', xml) : ''}>
652
+ Your browser does not support the audio tag.
653
+ </audio>
654
+ </div>
655
+ </div>`
656
+ }
657
+
658
+ async convert_colist(node) {
659
+ const result = []
660
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
661
+ const classes = ['colist', node.style, node.role].filter(Boolean)
662
+ const classAttribute = ` class="${classes.join(' ')}"`
663
+
664
+ result.push(`<div${idAttribute}${classAttribute}>`)
665
+ if (node.hasTitle()) result.push(`<div class="title">${node.title}</div>`)
666
+
667
+ if (node.document.hasAttribute('icons')) {
668
+ result.push('<table>')
669
+ const fontIcons = node.document.hasAttribute('icons', 'font')
670
+ let num = 0
671
+ for (const item of node.getItems()) {
672
+ num++
673
+ let numLabel
674
+ if (fontIcons) {
675
+ numLabel = `<i class="conum" data-value="${num}"></i><b>${num}</b>`
676
+ } else {
677
+ numLabel = `<img src="${await node.iconUri(`callouts/${num}`)}" alt="${num}"${this._voidSlash}>`
678
+ }
679
+ result.push(`<tr>
680
+ <td>${numLabel}</td>
681
+ <td>${item.getText()}${item.hasBlocks() ? LF + (await item.content()) : ''}</td>
682
+ </tr>`)
683
+ }
684
+ result.push('</table>')
685
+ } else {
686
+ result.push('<ol>')
687
+ for (const item of node.getItems()) {
688
+ result.push(`<li>
689
+ <p>${item.getText()}</p>${item.hasBlocks() ? LF + (await item.content()) : ''}
690
+ </li>`)
691
+ }
692
+ result.push('</ol>')
693
+ }
694
+
695
+ result.push('</div>')
696
+ return result.join(LF)
697
+ }
698
+
699
+ async convert_dlist(node) {
700
+ const result = []
701
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
702
+ let classes
703
+ switch (node.style) {
704
+ case 'qanda':
705
+ classes = ['qlist', 'qanda', node.role]
706
+ break
707
+ case 'horizontal':
708
+ classes = ['hdlist', node.role]
709
+ break
710
+ default:
711
+ classes = ['dlist', node.style, node.role]
712
+ }
713
+ const classAttribute = ` class="${classes.filter(Boolean).join(' ')}"`
714
+
715
+ result.push(`<div${idAttribute}${classAttribute}>`)
716
+ if (node.hasTitle()) result.push(`<div class="title">${node.title}</div>`)
717
+
718
+ switch (node.style) {
719
+ case 'qanda':
720
+ result.push('<ol>')
721
+ for (const [terms, dd] of node.getItems()) {
722
+ result.push('<li>')
723
+ for (const dt of terms) {
724
+ result.push(`<p><em>${dt.getText()}</em></p>`)
725
+ }
726
+ if (dd) {
727
+ if (dd.hasText()) result.push(`<p>${dd.getText()}</p>`)
728
+ if (dd.hasBlocks()) result.push(await dd.content())
729
+ }
730
+ result.push('</li>')
731
+ }
732
+ result.push('</ol>')
733
+ break
734
+ case 'horizontal': {
735
+ const slash = this._voidSlash
736
+ result.push('<table>')
737
+ if (node.hasAttribute('labelwidth') || node.hasAttribute('itemwidth')) {
738
+ result.push('<colgroup>')
739
+ const labelWidthAttr = node.hasAttribute('labelwidth')
740
+ ? ` width="${node.getAttribute('labelwidth').replace(/%$/, '')}%"`
741
+ : ''
742
+ result.push(`<col${labelWidthAttr}${slash}>`)
743
+ const itemWidthAttr = node.hasAttribute('itemwidth')
744
+ ? ` width="${node.getAttribute('itemwidth').replace(/%$/, '')}%"`
745
+ : ''
746
+ result.push(`<col${itemWidthAttr}${slash}>`)
747
+ result.push('</colgroup>')
748
+ }
749
+ for (const [terms, dd] of node.getItems()) {
750
+ result.push('<tr>')
751
+ result.push(
752
+ `<td class="hdlist1${node.hasOption('strong') ? ' strong' : ''}">`
753
+ )
754
+ let firstTerm = true
755
+ for (const dt of terms) {
756
+ if (!firstTerm) result.push(`<br${slash}>`)
757
+ result.push(dt.getText())
758
+ firstTerm = false
759
+ }
760
+ result.push('</td>')
761
+ result.push('<td class="hdlist2">')
762
+ if (dd) {
763
+ if (dd.hasText()) result.push(`<p>${dd.getText()}</p>`)
764
+ if (dd.hasBlocks()) result.push(await dd.content())
765
+ }
766
+ result.push('</td>')
767
+ result.push('</tr>')
768
+ }
769
+ result.push('</table>')
770
+ break
771
+ }
772
+ default: {
773
+ result.push('<dl>')
774
+ const dtStyleAttribute = node.style ? '' : ' class="hdlist1"'
775
+ for (const [terms, dd] of node.getItems()) {
776
+ for (const dt of terms) {
777
+ result.push(`<dt${dtStyleAttribute}>${dt.getText()}</dt>`)
778
+ }
779
+ if (!dd) continue
780
+ result.push('<dd>')
781
+ if (dd.hasText()) result.push(`<p>${dd.getText()}</p>`)
782
+ if (dd.hasBlocks()) result.push(await dd.content())
783
+ result.push('</dd>')
784
+ }
785
+ result.push('</dl>')
786
+ }
787
+ }
788
+
789
+ result.push('</div>')
790
+ return result.join(LF)
791
+ }
792
+
793
+ async convert_example(node) {
794
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
795
+ if (node.hasOption('collapsible')) {
796
+ const classAttribute = node.role ? ` class="${node.role}"` : ''
797
+ const summaryElement = node.hasTitle()
798
+ ? `<summary class="title">${node.title}</summary>`
799
+ : '<summary class="title">Details</summary>'
800
+ return `<details${idAttribute}${classAttribute}${node.hasOption('open') ? ' open' : ''}>
801
+ ${summaryElement}
802
+ <div class="content">
803
+ ${await node.content()}
804
+ </div>
805
+ </details>`
806
+ }
807
+ const titleElement = node.hasTitle()
808
+ ? `<div class="title">${node.captionedTitle()}</div>\n`
809
+ : ''
810
+ const role = node.role
811
+ return `<div${idAttribute} class="exampleblock${role ? ` ${role}` : ''}">
812
+ ${titleElement}<div class="content">
813
+ ${await node.content()}
814
+ </div>
815
+ </div>`
816
+ }
817
+
818
+ async convert_floating_title(node) {
819
+ const tagName = `h${node.level + 1}`
820
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
821
+ const classes = [node.style, node.role].filter(Boolean)
822
+ return `<${tagName}${idAttribute} class="${classes.join(' ')}">${node.title}</${tagName}>`
823
+ }
824
+
825
+ async convert_image(node) {
826
+ const target = node.getAttribute('target')
827
+ const widthAttr = node.hasAttribute('width')
828
+ ? ` width="${node.getAttribute('width')}"`
829
+ : ''
830
+ const heightAttr = node.hasAttribute('height')
831
+ ? ` height="${node.getAttribute('height')}"`
832
+ : ''
833
+ const slash = this._voidSlash
834
+ let img, src
835
+ if (
836
+ (node.hasAttribute('format', 'svg') || target.includes('.svg')) &&
837
+ node.document.safe < SafeMode.SECURE
838
+ ) {
839
+ if (node.hasOption('inline')) {
840
+ img =
841
+ (await this.readSvgContents(node, target)) ||
842
+ `<span class="alt">${node.getAlt()}</span>`
843
+ } else if (
844
+ node.hasOption('interactive') &&
845
+ node.document.safe >= SafeMode.SERVER
846
+ ) {
847
+ const fallback = node.hasAttribute('fallback')
848
+ ? `<img src="${await node.imageUri(node.getAttribute('fallback'))}" alt="${this._encodeAttrValue(node.getAlt())}"${widthAttr}${heightAttr}${slash}>`
849
+ : `<span class="alt">${node.getAlt()}</span>`
850
+ src = await node.imageUri(target)
851
+ img = `<object type="image/svg+xml" data="${src}"${widthAttr}${heightAttr}>${fallback}</object>`
852
+ } else {
853
+ src = await node.imageUri(target)
854
+ img = `<img src="${src}" alt="${this._encodeAttrValue(node.getAlt())}"${widthAttr}${heightAttr}${slash}>`
855
+ }
856
+ } else {
857
+ src = await node.imageUri(target)
858
+ img = `<img src="${src}" alt="${this._encodeAttrValue(node.getAlt())}"${widthAttr}${heightAttr}${slash}>`
859
+ }
860
+
861
+ if (node.hasAttribute('link')) {
862
+ let hrefAttrVal = node.getAttribute('link')
863
+ if (hrefAttrVal === 'self') hrefAttrVal = src
864
+ if (hrefAttrVal) {
865
+ img = `<a class="image" href="${hrefAttrVal}"${this._appendLinkConstraintAttrs(node).join('')}>${img}</a>`
866
+ }
867
+ }
868
+
869
+ const idAttr = node.id ? ` id="${node.id}"` : ''
870
+ const classes = ['imageblock']
871
+ if (node.hasAttribute('float')) classes.push(node.getAttribute('float'))
872
+ if (node.hasAttribute('align'))
873
+ classes.push(`text-${node.getAttribute('align')}`)
874
+ if (node.role) classes.push(node.role)
875
+ const classAttr = ` class="${classes.join(' ')}"`
876
+ const titleEl = node.hasTitle()
877
+ ? `\n<div class="title">${node.captionedTitle()}</div>`
878
+ : ''
879
+ return `<div${idAttr}${classAttr}>
880
+ <div class="content">
881
+ ${img}
882
+ </div>${titleEl}
883
+ </div>`
884
+ }
885
+
886
+ async convert_listing(node) {
887
+ const nowrap =
888
+ node.hasOption('nowrap') || !node.document.hasAttribute('prewrap')
889
+ let preOpen, preClose, syntaxHl, lang, opts
890
+ if (node.style === 'source') {
891
+ lang = node.getAttribute('language')
892
+ syntaxHl = node.document.syntaxHighlighter
893
+ if (syntaxHl) {
894
+ if (syntaxHl.handlesHighlighting()) {
895
+ const docAttrs = node.document.attributes
896
+ opts = {
897
+ css_mode: docAttrs[`${syntaxHl.name}-css`] || 'class',
898
+ style: docAttrs[`${syntaxHl.name}-style`],
899
+ }
900
+ } else {
901
+ opts = {}
902
+ }
903
+ opts.nowrap = nowrap
904
+ } else {
905
+ preOpen = `<pre class="highlight${nowrap ? ' nowrap' : ''}"><code${lang ? ` class="language-${lang}" data-lang="${lang}"` : ''}>`
906
+ preClose = '</code></pre>'
907
+ }
908
+ } else {
909
+ preOpen = `<pre${nowrap ? ' class="nowrap"' : ''}>`
910
+ preClose = '</pre>'
911
+ }
912
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
913
+ const titleElement = node.hasTitle()
914
+ ? `<div class="title">${node.captionedTitle()}</div>\n`
915
+ : ''
916
+ const role = node.role
917
+ const inner = syntaxHl
918
+ ? await syntaxHl.format(node, lang, opts)
919
+ : `${preOpen}${await node.content()}${preClose}`
920
+ return `<div${idAttribute} class="listingblock${role ? ` ${role}` : ''}">
921
+ ${titleElement}<div class="content">
922
+ ${inner}
923
+ </div>
924
+ </div>`
925
+ }
926
+
927
+ async convert_literal(node) {
928
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
929
+ const titleElement = node.hasTitle()
930
+ ? `<div class="title">${node.title}</div>\n`
931
+ : ''
932
+ const nowrap =
933
+ !node.document.hasAttribute('prewrap') || node.hasOption('nowrap')
934
+ const role = node.role
935
+ return `<div${idAttribute} class="literalblock${role ? ` ${role}` : ''}">
936
+ ${titleElement}<div class="content">
937
+ <pre${nowrap ? ' class="nowrap"' : ''}>${await node.content()}</pre>
938
+ </div>
939
+ </div>`
940
+ }
941
+
942
+ async convert_stem(node) {
943
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
944
+ const titleElement = node.hasTitle()
945
+ ? `<div class="title">${node.title}</div>\n`
946
+ : ''
947
+ const style = node.style
948
+ const [open, close] = BLOCK_MATH_DELIMITERS[style] ?? ['', '']
949
+ let equation = await node.content()
950
+ if (equation) {
951
+ if (style === 'asciimath' && equation.includes(LF)) {
952
+ const br = `${LF}<br${this._voidSlash}>`
953
+ equation = equation.replace(StemBreakRx, (match) => {
954
+ const newlineCount = (match.match(/\n/g) || []).length
955
+ // Blank lines (\n\n+) produce newlineCount <br>; escaped newlines produce newlineCount - 1.
956
+ const brCount = match[0] === '\n' ? newlineCount : newlineCount - 1
957
+ return `${close}${br.repeat(brCount)}${LF}${open}`
958
+ })
959
+ }
960
+ if (!equation.startsWith(open) || !equation.endsWith(close)) {
961
+ equation = `${open}${equation}${close}`
962
+ }
963
+ } else {
964
+ equation = ''
965
+ }
966
+ const role = node.role
967
+ return `<div${idAttribute} class="stemblock${role ? ` ${role}` : ''}">
968
+ ${titleElement}<div class="content">
969
+ ${equation}
970
+ </div>
971
+ </div>`
972
+ }
973
+
974
+ async convert_olist(node) {
975
+ const result = []
976
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
977
+ const classes = ['olist', node.style, node.role].filter(Boolean)
978
+ const classAttribute = ` class="${classes.join(' ')}"`
979
+
980
+ result.push(`<div${idAttribute}${classAttribute}>`)
981
+ if (node.hasTitle()) result.push(`<div class="title">${node.title}</div>`)
982
+
983
+ const keyword = node.listMarkerKeyword()
984
+ const typeAttribute = keyword ? ` type="${keyword}"` : ''
985
+ const startAttribute = node.hasAttribute('start')
986
+ ? ` start="${node.getAttribute('start')}"`
987
+ : ''
988
+ const reversedAttribute = node.hasOption('reversed')
989
+ ? this._appendBooleanAttr('reversed', this._xmlMode)
990
+ : ''
991
+ result.push(
992
+ `<ol class="${node.style}"${typeAttribute}${startAttribute}${reversedAttribute}>`
993
+ )
994
+
995
+ for (const item of node.getItems()) {
996
+ if (item.id) {
997
+ result.push(
998
+ `<li id="${item.id}"${item.role ? ` class="${item.role}"` : ''}>`
999
+ )
1000
+ } else if (item.role) {
1001
+ result.push(`<li class="${item.role}">`)
1002
+ } else {
1003
+ result.push('<li>')
1004
+ }
1005
+ result.push(`<p>${item.getText()}</p>`)
1006
+ if (item.hasBlocks()) result.push(await item.content())
1007
+ result.push('</li>')
1008
+ }
1009
+
1010
+ result.push('</ol>')
1011
+ result.push('</div>')
1012
+ return result.join(LF)
1013
+ }
1014
+
1015
+ async convert_open(node) {
1016
+ const style = node.style
1017
+ if (style === 'abstract') {
1018
+ if (
1019
+ node.getParent() === node.document &&
1020
+ node.document.doctype === 'book'
1021
+ ) {
1022
+ this.logger.warn(
1023
+ 'abstract block cannot be used in a document without a doctitle when doctype is book. Excluding block content.'
1024
+ )
1025
+ return ''
1026
+ }
1027
+ const idAttr = node.id ? ` id="${node.id}"` : ''
1028
+ const titleEl = node.hasTitle()
1029
+ ? `<div class="title">${node.title}</div>\n`
1030
+ : ''
1031
+ const role = node.role
1032
+ return `<div${idAttr} class="quoteblock abstract${role ? ` ${role}` : ''}">
1033
+ ${titleEl}<blockquote>
1034
+ ${await node.content()}
1035
+ </blockquote>
1036
+ </div>`
1037
+ }
1038
+ if (
1039
+ style === 'partintro' &&
1040
+ (node.level > 0 ||
1041
+ node.getParent().context !== 'section' ||
1042
+ node.document.doctype !== 'book')
1043
+ ) {
1044
+ this.logger.error(
1045
+ 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
1046
+ )
1047
+ return ''
1048
+ }
1049
+ const idAttr = node.id ? ` id="${node.id}"` : ''
1050
+ const titleEl = node.hasTitle()
1051
+ ? `<div class="title">${node.title}</div>\n`
1052
+ : ''
1053
+ const role = node.role
1054
+ return `<div${idAttr} class="openblock${style && style !== 'open' ? ` ${style}` : ''}${role ? ` ${role}` : ''}">
1055
+ ${titleEl}<div class="content">
1056
+ ${await node.content()}
1057
+ </div>
1058
+ </div>`
1059
+ }
1060
+
1061
+ async convert_page_break(_node) {
1062
+ return '<div class="page-break"></div>'
1063
+ }
1064
+
1065
+ async convert_paragraph(node) {
1066
+ let attributes
1067
+ if (node.role) {
1068
+ attributes = `${node.id ? ` id="${node.id}"` : ''} class="paragraph ${node.role}"`
1069
+ } else if (node.id) {
1070
+ attributes = ` id="${node.id}" class="paragraph"`
1071
+ } else {
1072
+ attributes = ' class="paragraph"'
1073
+ }
1074
+ if (node.hasTitle()) {
1075
+ return `<div${attributes}>
1076
+ <div class="title">${node.title}</div>
1077
+ <p>${await node.content()}</p>
1078
+ </div>`
1079
+ }
1080
+ return `<div${attributes}>
1081
+ <p>${await node.content()}</p>
1082
+ </div>`
1083
+ }
1084
+
1085
+ // alias convert_pass → content_only
1086
+ async convert_pass(node) {
1087
+ return this.contentOnly(node)
1088
+ }
1089
+
1090
+ async convert_preamble(node) {
1091
+ let toc = ''
1092
+ const doc = node.document
1093
+ if (
1094
+ doc.hasAttribute('toc-placement', 'preamble') &&
1095
+ doc.hasSections() &&
1096
+ doc.hasAttribute('toc')
1097
+ ) {
1098
+ toc = `
1099
+ <div id="toc" class="${doc.getAttribute('toc-class', 'toc')}">
1100
+ <div id="toctitle">${doc.getAttribute('toc-title')}</div>
1101
+ ${await doc.converter.convert(doc, 'outline')}
1102
+ </div>`
1103
+ }
1104
+ return `<div id="preamble">
1105
+ <div class="sectionbody">
1106
+ ${await node.content()}
1107
+ </div>${toc}
1108
+ </div>`
1109
+ }
1110
+
1111
+ async convert_quote(node) {
1112
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1113
+ const classes = ['quoteblock', node.role].filter(Boolean)
1114
+ const classAttribute = ` class="${classes.join(' ')}"`
1115
+ const titleElement = node.hasTitle()
1116
+ ? `\n<div class="title">${node.title}</div>`
1117
+ : ''
1118
+ const attribution = node.hasAttribute('attribution')
1119
+ ? node.getAttribute('attribution')
1120
+ : null
1121
+ const citetitle = node.hasAttribute('citetitle')
1122
+ ? node.getAttribute('citetitle')
1123
+ : null
1124
+ let attributionElement = ''
1125
+ if (attribution || citetitle) {
1126
+ const citeElement = citetitle ? `<cite>${citetitle}</cite>` : ''
1127
+ const attributionText = attribution
1128
+ ? `&#8212; ${attribution}${citetitle ? `<br${this._voidSlash}>\n` : ''}`
1129
+ : ''
1130
+ attributionElement = `\n<div class="attribution">\n${attributionText}${citeElement}\n</div>`
1131
+ }
1132
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1133
+ <blockquote>
1134
+ ${await node.content()}
1135
+ </blockquote>${attributionElement}
1136
+ </div>`
1137
+ }
1138
+
1139
+ async convert_thematic_break(node) {
1140
+ const classAttribute = node.role ? ` class="${node.role}"` : ''
1141
+ return `<hr${classAttribute}${this._voidSlash}>`
1142
+ }
1143
+
1144
+ async convert_sidebar(node) {
1145
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1146
+ const titleElement = node.hasTitle()
1147
+ ? `<div class="title">${node.title}</div>\n`
1148
+ : ''
1149
+ const role = node.role
1150
+ return `<div${idAttribute} class="sidebarblock${role ? ` ${role}` : ''}">
1151
+ <div class="content">
1152
+ ${titleElement}${await node.content()}
1153
+ </div>
1154
+ </div>`
1155
+ }
1156
+
1157
+ async convert_table(node) {
1158
+ const result = []
1159
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1160
+ let frame = node.getAttribute('frame', 'all', 'table-frame')
1161
+ if (frame === 'topbot') frame = 'ends'
1162
+ const classes = [
1163
+ 'tableblock',
1164
+ `frame-${frame}`,
1165
+ `grid-${node.getAttribute('grid', 'all', 'table-grid')}`,
1166
+ ]
1167
+ const stripes = node.getAttribute('stripes', null, 'table-stripes')
1168
+ if (stripes) classes.push(`stripes-${stripes}`)
1169
+ let widthAttribute = ''
1170
+ const autowidth = node.hasOption('autowidth')
1171
+ if (autowidth && !node.hasAttribute('width')) {
1172
+ classes.push('fit-content')
1173
+ } else {
1174
+ const tablewidth = node.getAttribute('tablepcwidth')
1175
+ if (Number(tablewidth) === 100) {
1176
+ classes.push('stretch')
1177
+ } else {
1178
+ widthAttribute = ` width="${tablewidth}%"`
1179
+ }
1180
+ }
1181
+ if (node.hasAttribute('float')) classes.push(node.getAttribute('float'))
1182
+ if (node.role) classes.push(node.role)
1183
+ const classAttribute = ` class="${classes.join(' ')}"`
1184
+
1185
+ result.push(`<table${idAttribute}${classAttribute}${widthAttribute}>`)
1186
+ if (node.hasTitle())
1187
+ result.push(`<caption class="title">${node.captionedTitle()}</caption>`)
1188
+
1189
+ if (node.getAttribute('rowcount') > 0) {
1190
+ const slash = this._voidSlash
1191
+ result.push('<colgroup>')
1192
+ if (autowidth) {
1193
+ for (let i = 0; i < node.columns.length; i++)
1194
+ result.push(`<col${slash}>`)
1195
+ } else {
1196
+ for (const col of node.columns) {
1197
+ result.push(
1198
+ col.hasOption('autowidth')
1199
+ ? `<col${slash}>`
1200
+ : `<col width="${col.getAttribute('colpcwidth')}%"${slash}>`
1201
+ )
1202
+ }
1203
+ }
1204
+ result.push('</colgroup>')
1205
+
1206
+ for (const [tsec, rows] of node.rows.bySection()) {
1207
+ if (rows.length === 0) continue
1208
+ result.push(`<t${tsec}>`)
1209
+ for (const row of rows) {
1210
+ result.push('<tr>')
1211
+ for (const cell of row) {
1212
+ let cellContent
1213
+ if (tsec === 'head') {
1214
+ cellContent = cell.text
1215
+ } else {
1216
+ switch (cell.style) {
1217
+ case 'asciidoc':
1218
+ cellContent = `<div class="content">${await cell.content()}</div>`
1219
+ break
1220
+ case 'literal':
1221
+ cellContent = `<div class="literal"><pre>${cell.text}</pre></div>`
1222
+ break
1223
+ default: {
1224
+ const parts = await cell.content()
1225
+ cellContent =
1226
+ parts.length === 0
1227
+ ? ''
1228
+ : `<p class="tableblock">${parts.join('</p>\n<p class="tableblock">')}</p>`
1229
+ }
1230
+ }
1231
+ }
1232
+ const cellTagName =
1233
+ tsec === 'head' || cell.style === 'header' ? 'th' : 'td'
1234
+ const cellClassAttr = ` class="tableblock halign-${cell.getAttribute('halign')} valign-${cell.getAttribute('valign')}"`
1235
+ const cellColspanAttr = cell.colspan
1236
+ ? ` colspan="${cell.colspan}"`
1237
+ : ''
1238
+ const cellRowspanAttr = cell.rowspan
1239
+ ? ` rowspan="${cell.rowspan}"`
1240
+ : ''
1241
+ // Use the per-cell captured cellbgcolor (set by {set:cellbgcolor:...} in cell text
1242
+ // during precomputeText). Fall back to the current document attribute if not captured.
1243
+ const cellbgcolor =
1244
+ '_cellbgcolor' in cell
1245
+ ? cell._cellbgcolor
1246
+ : node.document.attributes.cellbgcolor
1247
+ const cellStyleAttr = cellbgcolor
1248
+ ? ` style="background-color: ${cellbgcolor};"`
1249
+ : ''
1250
+ result.push(
1251
+ `<${cellTagName}${cellClassAttr}${cellColspanAttr}${cellRowspanAttr}${cellStyleAttr}>${cellContent}</${cellTagName}>`
1252
+ )
1253
+ }
1254
+ result.push('</tr>')
1255
+ }
1256
+ result.push(`</t${tsec}>`)
1257
+ }
1258
+ }
1259
+ result.push('</table>')
1260
+ return result.join(LF)
1261
+ }
1262
+
1263
+ async convert_toc(node) {
1264
+ const doc = node.document
1265
+ if (
1266
+ !doc.hasAttribute('toc-placement', 'macro') ||
1267
+ !doc.hasSections() ||
1268
+ !doc.hasAttribute('toc')
1269
+ ) {
1270
+ return '<!-- toc disabled -->'
1271
+ }
1272
+ let idAttr, titleIdAttr
1273
+ if (node.id) {
1274
+ idAttr = ` id="${node.id}"`
1275
+ titleIdAttr = ` id="${node.id}title"`
1276
+ } else {
1277
+ idAttr = ' id="toc"'
1278
+ titleIdAttr = ' id="toctitle"'
1279
+ }
1280
+ const title = node.hasTitle() ? node.title : doc.getAttribute('toc-title')
1281
+ const levels = node.hasAttribute('levels')
1282
+ ? parseInt(node.getAttribute('levels'), 10)
1283
+ : null
1284
+ const role = node.hasRoleAttribute()
1285
+ ? node.role
1286
+ : doc.getAttribute('toc-class', 'toc')
1287
+ return `<div${idAttr} class="${role}">
1288
+ <div${titleIdAttr} class="title">${title}</div>
1289
+ ${await doc.converter.convert(doc, 'outline', levels != null ? { toclevels: levels } : {})}
1290
+ </div>`
1291
+ }
1292
+
1293
+ async convert_ulist(node) {
1294
+ const result = []
1295
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1296
+ const divClasses = ['ulist', node.style, node.role].filter(Boolean)
1297
+ let markerChecked = ''
1298
+ let markerUnchecked = ''
1299
+ let ulClassAttribute
1300
+ const checklist = node.hasOption('checklist')
1301
+ if (checklist) {
1302
+ divClasses.splice(1, 0, 'checklist')
1303
+ ulClassAttribute = ' class="checklist"'
1304
+ if (node.hasOption('interactive')) {
1305
+ if (this._xmlMode) {
1306
+ markerChecked =
1307
+ '<input type="checkbox" data-item-complete="1" checked="checked"/> '
1308
+ markerUnchecked = '<input type="checkbox" data-item-complete="0"/> '
1309
+ } else {
1310
+ markerChecked =
1311
+ '<input type="checkbox" data-item-complete="1" checked> '
1312
+ markerUnchecked = '<input type="checkbox" data-item-complete="0"> '
1313
+ }
1314
+ } else if (node.document.hasAttribute('icons', 'font')) {
1315
+ markerChecked = '<i class="fa fa-check-square-o"></i> '
1316
+ markerUnchecked = '<i class="fa fa-square-o"></i> '
1317
+ } else {
1318
+ markerChecked = '&#10003; '
1319
+ markerUnchecked = '&#10063; '
1320
+ }
1321
+ } else {
1322
+ ulClassAttribute = node.style ? ` class="${node.style}"` : ''
1323
+ }
1324
+ result.push(`<div${idAttribute} class="${divClasses.join(' ')}">`)
1325
+ if (node.hasTitle()) result.push(`<div class="title">${node.title}</div>`)
1326
+ result.push(`<ul${ulClassAttribute}>`)
1327
+
1328
+ for (const item of node.getItems()) {
1329
+ if (item.id) {
1330
+ result.push(
1331
+ `<li id="${item.id}"${item.role ? ` class="${item.role}"` : ''}>`
1332
+ )
1333
+ } else if (item.role) {
1334
+ result.push(`<li class="${item.role}">`)
1335
+ } else {
1336
+ result.push('<li>')
1337
+ }
1338
+ if (checklist && item.hasAttribute('checkbox')) {
1339
+ result.push(
1340
+ `<p>${item.hasAttribute('checked') ? markerChecked : markerUnchecked}${item.getText()}</p>`
1341
+ )
1342
+ } else {
1343
+ result.push(`<p>${item.getText()}</p>`)
1344
+ }
1345
+ if (item.hasBlocks()) result.push(await item.content())
1346
+ result.push('</li>')
1347
+ }
1348
+
1349
+ result.push('</ul>')
1350
+ result.push('</div>')
1351
+ return result.join(LF)
1352
+ }
1353
+
1354
+ async convert_verse(node) {
1355
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1356
+ const classes = ['verseblock', node.role].filter(Boolean)
1357
+ const classAttribute = ` class="${classes.join(' ')}"`
1358
+ const titleElement = node.hasTitle()
1359
+ ? `\n<div class="title">${node.title}</div>`
1360
+ : ''
1361
+ const attribution = node.hasAttribute('attribution')
1362
+ ? node.getAttribute('attribution')
1363
+ : null
1364
+ const citetitle = node.hasAttribute('citetitle')
1365
+ ? node.getAttribute('citetitle')
1366
+ : null
1367
+ let attributionElement = ''
1368
+ if (attribution || citetitle) {
1369
+ const citeElement = citetitle ? `<cite>${citetitle}</cite>` : ''
1370
+ const attributionText = attribution
1371
+ ? `&#8212; ${attribution}${citetitle ? `<br${this._voidSlash}>\n` : ''}`
1372
+ : ''
1373
+ attributionElement = `\n<div class="attribution">\n${attributionText}${citeElement}\n</div>`
1374
+ }
1375
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1376
+ <pre class="content">${await node.content()}</pre>${attributionElement}
1377
+ </div>`
1378
+ }
1379
+
1380
+ async convert_video(node) {
1381
+ const xml = this._xmlMode
1382
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1383
+ const classes = ['videoblock']
1384
+ if (node.hasAttribute('float')) classes.push(node.getAttribute('float'))
1385
+ if (node.hasAttribute('align'))
1386
+ classes.push(`text-${node.getAttribute('align')}`)
1387
+ if (node.role) classes.push(node.role)
1388
+ const classAttribute = ` class="${classes.join(' ')}"`
1389
+ const titleElement = node.hasTitle()
1390
+ ? `\n<div class="title">${node.title}</div>`
1391
+ : ''
1392
+ const widthAttribute = node.hasAttribute('width')
1393
+ ? ` width="${node.getAttribute('width')}"`
1394
+ : ''
1395
+ const heightAttribute = node.hasAttribute('height')
1396
+ ? ` height="${node.getAttribute('height')}"`
1397
+ : ''
1398
+
1399
+ switch (node.getAttribute('poster')) {
1400
+ case 'vimeo': {
1401
+ let assetUriScheme = node.document.getAttribute(
1402
+ 'asset-uri-scheme',
1403
+ 'https'
1404
+ )
1405
+ if (assetUriScheme) assetUriScheme = `${assetUriScheme}:`
1406
+ const startAnchor = node.hasAttribute('start')
1407
+ ? `#at=${node.getAttribute('start')}`
1408
+ : ''
1409
+ const delimiter = ['?']
1410
+ let [target, hash] = node.getAttribute('target').split('/', 2)
1411
+ hash ||= node.getAttribute('hash')
1412
+ const hashParam = hash ? `${delimiter.pop() || '&amp;'}h=${hash}` : ''
1413
+ const autoplayParam = node.hasOption('autoplay')
1414
+ ? `${delimiter.pop() || '&amp;'}autoplay=1`
1415
+ : ''
1416
+ const loopParam = node.hasOption('loop')
1417
+ ? `${delimiter.pop() || '&amp;'}loop=1`
1418
+ : ''
1419
+ const mutedParam = node.hasOption('muted')
1420
+ ? `${delimiter.pop() || '&amp;'}muted=1`
1421
+ : ''
1422
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1423
+ <div class="content">
1424
+ <iframe${widthAttribute}${heightAttribute} src="${assetUriScheme}//player.vimeo.com/video/${target}${hashParam}${autoplayParam}${loopParam}${mutedParam}${startAnchor}" frameborder="0"${node.hasOption('nofullscreen') ? '' : this._appendBooleanAttr('allowfullscreen', xml)}></iframe>
1425
+ </div>
1426
+ </div>`
1427
+ }
1428
+ case 'youtube': {
1429
+ let assetUriScheme = node.document.getAttribute(
1430
+ 'asset-uri-scheme',
1431
+ 'https'
1432
+ )
1433
+ if (assetUriScheme) assetUriScheme = `${assetUriScheme}:`
1434
+ const relParamVal = node.hasOption('related') ? 1 : 0
1435
+ const startParam = node.hasAttribute('start')
1436
+ ? `&amp;start=${node.getAttribute('start')}`
1437
+ : ''
1438
+ const endParam = node.hasAttribute('end')
1439
+ ? `&amp;end=${node.getAttribute('end')}`
1440
+ : ''
1441
+ const autoplayParam = node.hasOption('autoplay')
1442
+ ? '&amp;autoplay=1'
1443
+ : ''
1444
+ const hasLoopParam = node.hasOption('loop')
1445
+ const loopParam = hasLoopParam ? '&amp;loop=1' : ''
1446
+ const muteParam = node.hasOption('muted') ? '&amp;mute=1' : ''
1447
+ const controlsParam = node.hasOption('nocontrols')
1448
+ ? '&amp;controls=0'
1449
+ : ''
1450
+ let fsParam, fsAttribute
1451
+ if (node.hasOption('nofullscreen')) {
1452
+ fsParam = '&amp;fs=0'
1453
+ fsAttribute = ''
1454
+ } else {
1455
+ fsParam = ''
1456
+ fsAttribute = this._appendBooleanAttr('allowfullscreen', xml)
1457
+ }
1458
+ const modestParam = node.hasOption('modest')
1459
+ ? '&amp;modestbranding=1'
1460
+ : ''
1461
+ const themeParam = node.hasAttribute('theme')
1462
+ ? `&amp;theme=${node.getAttribute('theme')}`
1463
+ : ''
1464
+ const hlParam = node.hasAttribute('lang')
1465
+ ? `&amp;hl=${node.getAttribute('lang')}`
1466
+ : ''
1467
+ let [target, list] = node.getAttribute('target').split('/', 2)
1468
+ list ||= node.getAttribute('list')
1469
+ let listParam
1470
+ if (list) {
1471
+ listParam = `&amp;list=${list}`
1472
+ } else {
1473
+ let playlist
1474
+ const videoParts = target.split(',')
1475
+ target = videoParts[0]
1476
+ playlist =
1477
+ videoParts.length > 1 ? videoParts.slice(1).join(',') : null
1478
+ playlist ||= node.getAttribute('playlist')
1479
+ if (playlist) {
1480
+ listParam = `&amp;playlist=${target},${playlist}`
1481
+ } else {
1482
+ listParam = hasLoopParam ? `&amp;playlist=${target}` : ''
1483
+ }
1484
+ }
1485
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1486
+ <div class="content">
1487
+ <iframe${widthAttribute}${heightAttribute} src="${assetUriScheme}//www.youtube.com/embed/${target}?rel=${relParamVal}${startParam}${endParam}${autoplayParam}${loopParam}${muteParam}${controlsParam}${listParam}${fsParam}${modestParam}${themeParam}${hlParam}" frameborder="0"${fsAttribute}></iframe>
1488
+ </div>
1489
+ </div>`
1490
+ }
1491
+ case 'wistia': {
1492
+ let assetUriScheme = node.document.getAttribute(
1493
+ 'asset-uri-scheme',
1494
+ 'https'
1495
+ )
1496
+ if (assetUriScheme) assetUriScheme = `${assetUriScheme}:`
1497
+ const delimiter = ['?']
1498
+ const startAnchor = node.hasAttribute('start')
1499
+ ? `${delimiter.pop() || '&amp;'}time=${node.getAttribute('start')}`
1500
+ : ''
1501
+ const endVideoBehaviorParam = node.hasOption('loop')
1502
+ ? `${delimiter.pop() || '&amp;'}endVideoBehavior=loop`
1503
+ : node.hasOption('reset')
1504
+ ? `${delimiter.pop() || '&amp;'}endVideoBehavior=reset`
1505
+ : ''
1506
+ const target = node.getAttribute('target')
1507
+ const autoplayParam = node.hasOption('autoplay')
1508
+ ? `${delimiter.pop() || '&amp;'}autoPlay=true`
1509
+ : ''
1510
+ const mutedParam = node.hasOption('muted')
1511
+ ? `${delimiter.pop() || '&amp;'}muted=true`
1512
+ : ''
1513
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1514
+ <div class="content">
1515
+ <iframe${widthAttribute}${heightAttribute} src="${assetUriScheme}//fast.wistia.com/embed/iframe/${target}${startAnchor}${autoplayParam}${endVideoBehaviorParam}${mutedParam}" frameborder="0"${node.hasOption('nofullscreen') ? '' : this._appendBooleanAttr('allowfullscreen', xml)} class="wistia_embed" name="wistia_embed"></iframe>
1516
+ </div>
1517
+ </div>`
1518
+ }
1519
+ default: {
1520
+ const posterVal = node.getAttribute('poster')
1521
+ const posterAttribute = !posterVal
1522
+ ? ''
1523
+ : ` poster="${node.mediaUri(posterVal)}"`
1524
+ const preloadVal = node.getAttribute('preload')
1525
+ const preloadAttribute = !preloadVal ? '' : ` preload="${preloadVal}"`
1526
+ const startT = node.getAttribute('start')
1527
+ const endT = node.getAttribute('end')
1528
+ const timeAnchor =
1529
+ startT || endT ? `#t=${startT || ''}${endT ? `,${endT}` : ''}` : ''
1530
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1531
+ <div class="content">
1532
+ <video src="${node.mediaUri(node.getAttribute('target'))}${timeAnchor}"${widthAttribute}${heightAttribute}${posterAttribute}${node.hasOption('autoplay') ? this._appendBooleanAttr('autoplay', xml) : ''}${node.hasOption('muted') ? this._appendBooleanAttr('muted', xml) : ''}${node.hasOption('nocontrols') ? '' : this._appendBooleanAttr('controls', xml)}${node.hasOption('loop') ? this._appendBooleanAttr('loop', xml) : ''}${preloadAttribute}>
1533
+ Your browser does not support the video tag.
1534
+ </video>
1535
+ </div>
1536
+ </div>`
1537
+ }
1538
+ }
1539
+ }
1540
+
1541
+ async convert_inline_anchor(node) {
1542
+ switch (node.type) {
1543
+ case 'xref': {
1544
+ let attrs, text
1545
+ if (node.attributes.path) {
1546
+ attrs = this._appendLinkConstraintAttrs(
1547
+ node,
1548
+ node.role ? [` class="${node.role}"`] : []
1549
+ ).join('')
1550
+ text = node.text || node.attributes.path
1551
+ } else {
1552
+ attrs = node.role ? ` class="${node.role}"` : ''
1553
+ if (!(text = node.text)) {
1554
+ const refs = (this._refs ??= node.document.catalog.refs)
1555
+ const refid = node.attributes.refid
1556
+ let top
1557
+ const ref =
1558
+ refs[refid] ??
1559
+ (!refid ? (top = this._getRootDocument(node)) : null)
1560
+ if (ref instanceof AbstractNode) {
1561
+ const resolvingSet = (this._resolvingXrefs ??= new Set())
1562
+ if (!resolvingSet.has(refid)) {
1563
+ resolvingSet.add(refid)
1564
+ const resolved = await ref.xreftext(
1565
+ node.getAttribute('xrefstyle', null, true)
1566
+ )
1567
+ resolvingSet.delete(refid)
1568
+ if (resolved) {
1569
+ text = resolved.includes('<a')
1570
+ ? resolved.replace(new RegExp(DropAnchorRx.source, 'g'), '')
1571
+ : resolved
1572
+ } else {
1573
+ text = top ? '[^top]' : `[${refid}]`
1574
+ }
1575
+ } else {
1576
+ text = top ? '[^top]' : `[${refid}]`
1577
+ }
1578
+ } else {
1579
+ text = `[${refid}]`
1580
+ }
1581
+ }
1582
+ }
1583
+ return `<a href="${node.target}"${attrs}>${text}</a>`
1584
+ }
1585
+ case 'ref':
1586
+ return `<a id="${node.id}"></a>`
1587
+ case 'link': {
1588
+ const attrs = node.id ? [` id="${node.id}"`] : []
1589
+ if (node.role) attrs.push(` class="${node.role}"`)
1590
+ if (node.hasAttribute('title'))
1591
+ attrs.push(` title="${node.getAttribute('title')}"`)
1592
+ return `<a href="${node.target}"${this._appendLinkConstraintAttrs(node, attrs).join('')}>${node.text ?? ''}</a>`
1593
+ }
1594
+ case 'bibref':
1595
+ return `<a id="${node.id}"></a>[${node.reftext || node.id}]`
1596
+ default:
1597
+ this.logger.warn(`unknown anchor type: ${node.type}`)
1598
+ return null
1599
+ }
1600
+ }
1601
+
1602
+ async convert_inline_break(node) {
1603
+ return `${node.text}<br${this._voidSlash}>`
1604
+ }
1605
+
1606
+ async convert_inline_button(node) {
1607
+ return `<b class="button">${node.text}</b>`
1608
+ }
1609
+
1610
+ async convert_inline_callout(node) {
1611
+ if (node.document.hasAttribute('icons', 'font')) {
1612
+ return `<i class="conum" data-value="${node.text}"></i><b>(${node.text})</b>`
1613
+ }
1614
+ if (node.document.hasAttribute('icons')) {
1615
+ const src = await node.iconUri(`callouts/${node.text}`)
1616
+ return `<img src="${src}" alt="${node.text}"${this._voidSlash}>`
1617
+ }
1618
+ const guard = node.attributes.guard
1619
+ if (Array.isArray(guard)) {
1620
+ return `&lt;!--<b class="conum">(${node.text})</b>--&gt;`
1621
+ }
1622
+ return `${guard ?? ''}<b class="conum">(${node.text})</b>`
1623
+ }
1624
+
1625
+ async convert_inline_footnote(node) {
1626
+ const index = node.getAttribute('index')
1627
+ if (index) {
1628
+ if (node.type === 'xref') {
1629
+ return `<sup class="footnoteref">[<a class="footnote" href="#_footnotedef_${index}" title="View footnote.">${index}</a>]</sup>`
1630
+ }
1631
+ const idAttr = node.id ? ` id="_footnote_${node.id}"` : ''
1632
+ return `<sup class="footnote"${idAttr}>[<a id="_footnoteref_${index}" class="footnote" href="#_footnotedef_${index}" title="View footnote.">${index}</a>]</sup>`
1633
+ }
1634
+ if (node.type === 'xref') {
1635
+ return `<sup class="footnoteref red" title="Unresolved footnote reference.">[${node.text}]</sup>`
1636
+ }
1637
+ return null
1638
+ }
1639
+
1640
+ async convert_inline_image(node) {
1641
+ const target = node.target
1642
+ const type = node.type || 'image'
1643
+ let img, src
1644
+ if (type === 'icon') {
1645
+ const icons = node.document.getAttribute('icons')
1646
+ if (icons === 'font') {
1647
+ let iClassAttrVal = `fa fa-${target}`
1648
+ if (node.hasAttribute('size'))
1649
+ iClassAttrVal += ` fa-${node.getAttribute('size')}`
1650
+ if (node.hasAttribute('flip')) {
1651
+ iClassAttrVal += ` fa-flip-${node.getAttribute('flip')}`
1652
+ } else if (node.hasAttribute('rotate')) {
1653
+ iClassAttrVal += ` fa-rotate-${node.getAttribute('rotate')}`
1654
+ }
1655
+ const titleAttr = node.hasAttribute('title')
1656
+ ? ` title="${node.getAttribute('title')}"`
1657
+ : ''
1658
+ img = `<i class="${iClassAttrVal}"${titleAttr}></i>`
1659
+ } else if (icons != null) {
1660
+ let attrs = node.hasAttribute('width')
1661
+ ? ` width="${node.getAttribute('width')}"`
1662
+ : ''
1663
+ if (node.hasAttribute('height'))
1664
+ attrs += ` height="${node.getAttribute('height')}"`
1665
+ if (node.hasAttribute('title'))
1666
+ attrs += ` title="${node.getAttribute('title')}"`
1667
+ img = `<img src="${await node.iconUri(target)}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1668
+ } else {
1669
+ img = `[${node.getAlt()}&#93;`
1670
+ }
1671
+ } else {
1672
+ let attrs = node.hasAttribute('width')
1673
+ ? ` width="${node.getAttribute('width')}"`
1674
+ : ''
1675
+ if (node.hasAttribute('height'))
1676
+ attrs += ` height="${node.getAttribute('height')}"`
1677
+ if (node.hasAttribute('title'))
1678
+ attrs += ` title="${node.getAttribute('title')}"`
1679
+ if (
1680
+ (node.hasAttribute('format', 'svg') || target.includes('.svg')) &&
1681
+ node.document.safe < SafeMode.SECURE
1682
+ ) {
1683
+ if (node.hasOption('inline')) {
1684
+ img =
1685
+ (await this.readSvgContents(node, target)) ||
1686
+ `<span class="alt">${node.getAlt()}</span>`
1687
+ } else if (
1688
+ node.hasOption('interactive') &&
1689
+ node.document.safe >= SafeMode.SERVER
1690
+ ) {
1691
+ const fallback = node.hasAttribute('fallback')
1692
+ ? `<img src="${await node.imageUri(node.getAttribute('fallback'))}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1693
+ : `<span class="alt">${node.getAlt()}</span>`
1694
+ src = await node.imageUri(target)
1695
+ img = `<object type="image/svg+xml" data="${src}"${attrs}>${fallback}</object>`
1696
+ } else {
1697
+ src = await node.imageUri(target)
1698
+ img = `<img src="${src}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1699
+ }
1700
+ } else {
1701
+ src = await node.imageUri(target)
1702
+ img = `<img src="${src}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1703
+ }
1704
+ }
1705
+
1706
+ if (node.hasAttribute('link')) {
1707
+ let hrefAttrVal = node.getAttribute('link')
1708
+ if (hrefAttrVal === 'self') hrefAttrVal = src
1709
+ if (hrefAttrVal) {
1710
+ img = `<a class="image" href="${hrefAttrVal}"${this._appendLinkConstraintAttrs(node).join('')}>${img}</a>`
1711
+ }
1712
+ }
1713
+
1714
+ const idAttr = node.id ? ` id="${node.id}"` : ''
1715
+ let classAttrVal = type
1716
+ const role = node.role
1717
+ if (role) {
1718
+ classAttrVal = node.hasAttribute('float')
1719
+ ? `${classAttrVal} ${node.getAttribute('float')} ${role}`
1720
+ : `${classAttrVal} ${role}`
1721
+ } else if (node.hasAttribute('float')) {
1722
+ classAttrVal = `${classAttrVal} ${node.getAttribute('float')}`
1723
+ }
1724
+ return `<span${idAttr} class="${classAttrVal}">${img}</span>`
1725
+ }
1726
+
1727
+ async convert_inline_indexterm(node) {
1728
+ return node.type === 'visible' ? node.text : ''
1729
+ }
1730
+
1731
+ async convert_inline_kbd(node) {
1732
+ const keys = node.getAttribute('keys')
1733
+ if (keys.length === 1) {
1734
+ return `<kbd>${keys[0]}</kbd>`
1735
+ }
1736
+ return `<span class="keyseq"><kbd>${keys.join('</kbd>+<kbd>')}</kbd></span>`
1737
+ }
1738
+
1739
+ async convert_inline_menu(node) {
1740
+ const caret = node.document.hasAttribute('icons', 'font')
1741
+ ? '&#160;<i class="fa fa-angle-right caret"></i> '
1742
+ : '&#160;<b class="caret">&#8250;</b> '
1743
+ const submenuJoiner = `</b>${caret}<b class="submenu">`
1744
+ const menu = node.getAttribute('menu')
1745
+ const submenus = node.getAttribute('submenus')
1746
+ if (!submenus || submenus.length === 0) {
1747
+ const menuitem = node.getAttribute('menuitem')
1748
+ if (menuitem) {
1749
+ return `<span class="menuseq"><b class="menu">${menu}</b>${caret}<b class="menuitem">${menuitem}</b></span>`
1750
+ }
1751
+ return `<b class="menuref">${menu}</b>`
1752
+ }
1753
+ return `<span class="menuseq"><b class="menu">${menu}</b>${caret}<b class="submenu">${submenus.join(submenuJoiner)}</b>${caret}<b class="menuitem">${node.getAttribute('menuitem')}</b></span>`
1754
+ }
1755
+
1756
+ async convert_inline_quoted(node) {
1757
+ const [open, close, tag] = QUOTE_TAGS[node.type] ?? DEFAULT_QUOTE_TAG
1758
+ if (node.id) {
1759
+ const classAttr = node.role ? ` class="${node.role}"` : ''
1760
+ if (tag) {
1761
+ return `${open.slice(0, -1)} id="${node.id}"${classAttr}>${node.text}${close}`
1762
+ }
1763
+ return `<span id="${node.id}"${classAttr}>${open}${node.text}${close}</span>`
1764
+ }
1765
+ if (node.role) {
1766
+ if (tag) {
1767
+ return `${open.slice(0, -1)} class="${node.role}">${node.text}${close}`
1768
+ }
1769
+ return `<span class="${node.role}">${open}${node.text}${close}</span>`
1770
+ }
1771
+ return `${open}${node.text}${close}`
1772
+ }
1773
+
1774
+ // NOTE expose readSvgContents for Bespoke converter
1775
+ async readSvgContents(node, target) {
1776
+ const imagesdir = node.document.getAttribute('imagesdir')
1777
+ let resolvedPath
1778
+ let svg
1779
+ if (isUriish(target) || (imagesdir && isUriish(imagesdir))) {
1780
+ svg = await node.readContents(target, {
1781
+ start: imagesdir,
1782
+ normalize: true,
1783
+ warnOnFailure: true,
1784
+ label: 'SVG',
1785
+ })
1786
+ resolvedPath = target
1787
+ } else {
1788
+ resolvedPath = node.normalizeSystemPath(target, imagesdir, null, {
1789
+ targetName: 'image',
1790
+ })
1791
+ svg = await node.readAsset(resolvedPath, {
1792
+ normalize: true,
1793
+ warnOnFailure: true,
1794
+ label: 'SVG',
1795
+ })
1796
+ }
1797
+ if (svg == null) return null // file not found/readable; warning already emitted
1798
+ if (!svg) {
1799
+ node.logger.warn(`contents of SVG is empty: ${resolvedPath}`)
1800
+ return null
1801
+ }
1802
+ if (!svg.startsWith('<svg')) svg = svg.replace(SvgPreambleRx, '')
1803
+ // Fix incomplete SVG start tag (missing closing >) by inserting > before the first child element.
1804
+ // This handles cases like: <svg width="500"\n<circle .../> where the > is missing.
1805
+ svg = svg.replace(
1806
+ /^(<svg\b[^<>]*?)(\s*<[^/!])/s,
1807
+ (_, pre, rest) => `${pre.trimEnd()}>${rest}`
1808
+ )
1809
+ let oldStartTag = null
1810
+ let newStartTag = null
1811
+ let startTagMatch = null
1812
+ for (const dim of ['width', 'height']) {
1813
+ if (!node.hasAttribute(dim)) continue
1814
+ if (!newStartTag) {
1815
+ if (startTagMatch === null)
1816
+ startTagMatch = svg.match(SvgStartTagRx) || false
1817
+ if (!startTagMatch) continue
1818
+ oldStartTag = startTagMatch[0]
1819
+ newStartTag = oldStartTag.replace(
1820
+ new RegExp(DimensionAttributeRx.source, 'g'),
1821
+ ''
1822
+ )
1823
+ }
1824
+ newStartTag = `${newStartTag.slice(0, -1)} ${dim}="${node.getAttribute(dim)}">`
1825
+ }
1826
+ if (newStartTag) svg = `${newStartTag}${svg.slice(oldStartTag.length)}`
1827
+ return svg
1828
+ }
1829
+
1830
+ // ── Private helpers ─────────────────────────────────────────────────────────
1831
+
1832
+ /**
1833
+ * @internal
1834
+ * @private
1835
+ */
1836
+ _appendBooleanAttr(name, xml) {
1837
+ return xml ? ` ${name}="${name}"` : ` ${name}`
1838
+ }
1839
+
1840
+ /**
1841
+ * @internal
1842
+ * @private
1843
+ */
1844
+ _appendLinkConstraintAttrs(node, attrs = []) {
1845
+ const rel = node.hasOption('nofollow') ? 'nofollow' : null
1846
+ const window = node.attributes.window
1847
+ if (window) {
1848
+ attrs.push(` target="${window}"`)
1849
+ if (window === '_blank' || node.hasOption('noopener')) {
1850
+ attrs.push(rel ? ` rel="${rel} noopener"` : ' rel="noopener"')
1851
+ }
1852
+ } else if (rel) {
1853
+ attrs.push(` rel="${rel}"`)
1854
+ }
1855
+ return attrs
1856
+ }
1857
+
1858
+ /**
1859
+ * @internal
1860
+ * @private
1861
+ */
1862
+ _encodeAttrValue(val) {
1863
+ return val.includes('"') ? val.replace(/"/g, '&quot;') : val
1864
+ }
1865
+
1866
+ /**
1867
+ * @internal
1868
+ * @private
1869
+ */
1870
+ _generateMannameSection(node) {
1871
+ let mannameTitle = node.getAttribute('manname-title', 'Name')
1872
+ const sections = node.sections()
1873
+ if (sections.length > 0) {
1874
+ const nextSectionTitle = sections[0].title
1875
+ if (nextSectionTitle === nextSectionTitle.toUpperCase()) {
1876
+ mannameTitle = mannameTitle.toUpperCase()
1877
+ }
1878
+ }
1879
+ const mannameId = node.getAttribute('manname-id')
1880
+ const mannameIdAttr = mannameId ? ` id="${mannameId}"` : ''
1881
+ return `<h2${mannameIdAttr}>${mannameTitle}</h2>
1882
+ <div class="sectionbody">
1883
+ <p>${node.getAttribute('mannames').join(', ')} - ${node.getAttribute('manpurpose')}</p>
1884
+ </div>`
1885
+ }
1886
+
1887
+ /**
1888
+ * @internal
1889
+ * @private
1890
+ */
1891
+ _getRootDocument(node) {
1892
+ while ((node = node.document).isNested()) {
1893
+ node = node.parentDocument
1894
+ }
1895
+ return node
1896
+ }
1897
+ }
1898
+
1899
+ Html5Converter.registerFor('html5')