@asciidoctor/core 3.0.4 → 4.0.0-alpha.2

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 (92) hide show
  1. package/README.md +42 -10
  2. package/build/browser/index.js +24156 -0
  3. package/build/node/index.cjs +24737 -0
  4. package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
  5. package/package.json +55 -97
  6. package/src/abstract_block.js +857 -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 +1893 -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 +346 -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.cts +75 -0
  69. package/types/index.d.ts +73 -3731
  70. package/types/inline.d.ts +69 -0
  71. package/types/list.d.ts +114 -0
  72. package/types/load.d.ts +39 -0
  73. package/types/logging.d.ts +187 -0
  74. package/types/parser.d.ts +114 -0
  75. package/types/path_resolver.d.ts +103 -0
  76. package/types/reader.d.ts +184 -0
  77. package/types/rx.d.ts +513 -0
  78. package/types/section.d.ts +122 -0
  79. package/types/stylesheets.d.ts +10 -0
  80. package/types/substitutors.d.ts +208 -0
  81. package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
  82. package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
  83. package/types/syntax_highlighter.d.ts +167 -0
  84. package/types/table.d.ts +231 -0
  85. package/types/timings.d.ts +25 -0
  86. package/types/tsconfig.json +9 -0
  87. package/LICENSE +0 -21
  88. package/dist/browser/asciidoctor.js +0 -47654
  89. package/dist/browser/asciidoctor.min.js +0 -1452
  90. package/dist/graalvm/asciidoctor.js +0 -47402
  91. package/dist/node/asciidoctor.cjs +0 -21567
  92. package/dist/node/asciidoctor.js +0 -23037
@@ -0,0 +1,1893 @@
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 (node.hasOption('interactive')) {
844
+ const fallback = node.hasAttribute('fallback')
845
+ ? `<img src="${await node.imageUri(node.getAttribute('fallback'))}" alt="${this._encodeAttrValue(node.getAlt())}"${widthAttr}${heightAttr}${slash}>`
846
+ : `<span class="alt">${node.getAlt()}</span>`
847
+ src = await node.imageUri(target)
848
+ img = `<object type="image/svg+xml" data="${src}"${widthAttr}${heightAttr}>${fallback}</object>`
849
+ } else {
850
+ src = await node.imageUri(target)
851
+ img = `<img src="${src}" alt="${this._encodeAttrValue(node.getAlt())}"${widthAttr}${heightAttr}${slash}>`
852
+ }
853
+ } else {
854
+ src = await node.imageUri(target)
855
+ img = `<img src="${src}" alt="${this._encodeAttrValue(node.getAlt())}"${widthAttr}${heightAttr}${slash}>`
856
+ }
857
+
858
+ if (node.hasAttribute('link')) {
859
+ let hrefAttrVal = node.getAttribute('link')
860
+ if (hrefAttrVal === 'self') hrefAttrVal = src
861
+ if (hrefAttrVal) {
862
+ img = `<a class="image" href="${hrefAttrVal}"${this._appendLinkConstraintAttrs(node).join('')}>${img}</a>`
863
+ }
864
+ }
865
+
866
+ const idAttr = node.id ? ` id="${node.id}"` : ''
867
+ const classes = ['imageblock']
868
+ if (node.hasAttribute('float')) classes.push(node.getAttribute('float'))
869
+ if (node.hasAttribute('align'))
870
+ classes.push(`text-${node.getAttribute('align')}`)
871
+ if (node.role) classes.push(node.role)
872
+ const classAttr = ` class="${classes.join(' ')}"`
873
+ const titleEl = node.hasTitle()
874
+ ? `\n<div class="title">${node.captionedTitle()}</div>`
875
+ : ''
876
+ return `<div${idAttr}${classAttr}>
877
+ <div class="content">
878
+ ${img}
879
+ </div>${titleEl}
880
+ </div>`
881
+ }
882
+
883
+ async convert_listing(node) {
884
+ const nowrap =
885
+ node.hasOption('nowrap') || !node.document.hasAttribute('prewrap')
886
+ let preOpen, preClose, syntaxHl, lang, opts
887
+ if (node.style === 'source') {
888
+ lang = node.getAttribute('language')
889
+ syntaxHl = node.document.syntaxHighlighter
890
+ if (syntaxHl) {
891
+ if (syntaxHl.handlesHighlighting()) {
892
+ const docAttrs = node.document.attributes
893
+ opts = {
894
+ css_mode: docAttrs[`${syntaxHl.name}-css`] || 'class',
895
+ style: docAttrs[`${syntaxHl.name}-style`],
896
+ }
897
+ } else {
898
+ opts = {}
899
+ }
900
+ opts.nowrap = nowrap
901
+ } else {
902
+ preOpen = `<pre class="highlight${nowrap ? ' nowrap' : ''}"><code${lang ? ` class="language-${lang}" data-lang="${lang}"` : ''}>`
903
+ preClose = '</code></pre>'
904
+ }
905
+ } else {
906
+ preOpen = `<pre${nowrap ? ' class="nowrap"' : ''}>`
907
+ preClose = '</pre>'
908
+ }
909
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
910
+ const titleElement = node.hasTitle()
911
+ ? `<div class="title">${node.captionedTitle()}</div>\n`
912
+ : ''
913
+ const role = node.role
914
+ const inner = syntaxHl
915
+ ? await syntaxHl.format(node, lang, opts)
916
+ : `${preOpen}${await node.content()}${preClose}`
917
+ return `<div${idAttribute} class="listingblock${role ? ` ${role}` : ''}">
918
+ ${titleElement}<div class="content">
919
+ ${inner}
920
+ </div>
921
+ </div>`
922
+ }
923
+
924
+ async convert_literal(node) {
925
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
926
+ const titleElement = node.hasTitle()
927
+ ? `<div class="title">${node.title}</div>\n`
928
+ : ''
929
+ const nowrap =
930
+ !node.document.hasAttribute('prewrap') || node.hasOption('nowrap')
931
+ const role = node.role
932
+ return `<div${idAttribute} class="literalblock${role ? ` ${role}` : ''}">
933
+ ${titleElement}<div class="content">
934
+ <pre${nowrap ? ' class="nowrap"' : ''}>${await node.content()}</pre>
935
+ </div>
936
+ </div>`
937
+ }
938
+
939
+ async convert_stem(node) {
940
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
941
+ const titleElement = node.hasTitle()
942
+ ? `<div class="title">${node.title}</div>\n`
943
+ : ''
944
+ const style = node.style
945
+ const [open, close] = BLOCK_MATH_DELIMITERS[style] ?? ['', '']
946
+ let equation = await node.content()
947
+ if (equation) {
948
+ if (style === 'asciimath' && equation.includes(LF)) {
949
+ const br = `${LF}<br${this._voidSlash}>`
950
+ equation = equation.replace(StemBreakRx, (match) => {
951
+ const newlineCount = (match.match(/\n/g) || []).length
952
+ // Blank lines (\n\n+) produce newlineCount <br>; escaped newlines produce newlineCount - 1.
953
+ const brCount = match[0] === '\n' ? newlineCount : newlineCount - 1
954
+ return `${close}${br.repeat(brCount)}${LF}${open}`
955
+ })
956
+ }
957
+ if (!equation.startsWith(open) || !equation.endsWith(close)) {
958
+ equation = `${open}${equation}${close}`
959
+ }
960
+ } else {
961
+ equation = ''
962
+ }
963
+ const role = node.role
964
+ return `<div${idAttribute} class="stemblock${role ? ` ${role}` : ''}">
965
+ ${titleElement}<div class="content">
966
+ ${equation}
967
+ </div>
968
+ </div>`
969
+ }
970
+
971
+ async convert_olist(node) {
972
+ const result = []
973
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
974
+ const classes = ['olist', node.style, node.role].filter(Boolean)
975
+ const classAttribute = ` class="${classes.join(' ')}"`
976
+
977
+ result.push(`<div${idAttribute}${classAttribute}>`)
978
+ if (node.hasTitle()) result.push(`<div class="title">${node.title}</div>`)
979
+
980
+ const keyword = node.listMarkerKeyword()
981
+ const typeAttribute = keyword ? ` type="${keyword}"` : ''
982
+ const startAttribute = node.hasAttribute('start')
983
+ ? ` start="${node.getAttribute('start')}"`
984
+ : ''
985
+ const reversedAttribute = node.hasOption('reversed')
986
+ ? this._appendBooleanAttr('reversed', this._xmlMode)
987
+ : ''
988
+ result.push(
989
+ `<ol class="${node.style}"${typeAttribute}${startAttribute}${reversedAttribute}>`
990
+ )
991
+
992
+ for (const item of node.getItems()) {
993
+ if (item.id) {
994
+ result.push(
995
+ `<li id="${item.id}"${item.role ? ` class="${item.role}"` : ''}>`
996
+ )
997
+ } else if (item.role) {
998
+ result.push(`<li class="${item.role}">`)
999
+ } else {
1000
+ result.push('<li>')
1001
+ }
1002
+ result.push(`<p>${item.getText()}</p>`)
1003
+ if (item.hasBlocks()) result.push(await item.content())
1004
+ result.push('</li>')
1005
+ }
1006
+
1007
+ result.push('</ol>')
1008
+ result.push('</div>')
1009
+ return result.join(LF)
1010
+ }
1011
+
1012
+ async convert_open(node) {
1013
+ const style = node.style
1014
+ if (style === 'abstract') {
1015
+ if (
1016
+ node.getParent() === node.document &&
1017
+ node.document.doctype === 'book'
1018
+ ) {
1019
+ this.logger.warn(
1020
+ 'abstract block cannot be used in a document without a doctitle when doctype is book. Excluding block content.'
1021
+ )
1022
+ return ''
1023
+ }
1024
+ const idAttr = node.id ? ` id="${node.id}"` : ''
1025
+ const titleEl = node.hasTitle()
1026
+ ? `<div class="title">${node.title}</div>\n`
1027
+ : ''
1028
+ const role = node.role
1029
+ return `<div${idAttr} class="quoteblock abstract${role ? ` ${role}` : ''}">
1030
+ ${titleEl}<blockquote>
1031
+ ${await node.content()}
1032
+ </blockquote>
1033
+ </div>`
1034
+ }
1035
+ if (
1036
+ style === 'partintro' &&
1037
+ (node.level > 0 ||
1038
+ node.getParent().context !== 'section' ||
1039
+ node.document.doctype !== 'book')
1040
+ ) {
1041
+ this.logger.error(
1042
+ 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
1043
+ )
1044
+ return ''
1045
+ }
1046
+ const idAttr = node.id ? ` id="${node.id}"` : ''
1047
+ const titleEl = node.hasTitle()
1048
+ ? `<div class="title">${node.title}</div>\n`
1049
+ : ''
1050
+ const role = node.role
1051
+ return `<div${idAttr} class="openblock${style && style !== 'open' ? ` ${style}` : ''}${role ? ` ${role}` : ''}">
1052
+ ${titleEl}<div class="content">
1053
+ ${await node.content()}
1054
+ </div>
1055
+ </div>`
1056
+ }
1057
+
1058
+ async convert_page_break(_node) {
1059
+ return '<div class="page-break"></div>'
1060
+ }
1061
+
1062
+ async convert_paragraph(node) {
1063
+ let attributes
1064
+ if (node.role) {
1065
+ attributes = `${node.id ? ` id="${node.id}"` : ''} class="paragraph ${node.role}"`
1066
+ } else if (node.id) {
1067
+ attributes = ` id="${node.id}" class="paragraph"`
1068
+ } else {
1069
+ attributes = ' class="paragraph"'
1070
+ }
1071
+ if (node.hasTitle()) {
1072
+ return `<div${attributes}>
1073
+ <div class="title">${node.title}</div>
1074
+ <p>${await node.content()}</p>
1075
+ </div>`
1076
+ }
1077
+ return `<div${attributes}>
1078
+ <p>${await node.content()}</p>
1079
+ </div>`
1080
+ }
1081
+
1082
+ // alias convert_pass → content_only
1083
+ async convert_pass(node) {
1084
+ return this.contentOnly(node)
1085
+ }
1086
+
1087
+ async convert_preamble(node) {
1088
+ let toc = ''
1089
+ const doc = node.document
1090
+ if (
1091
+ doc.hasAttribute('toc-placement', 'preamble') &&
1092
+ doc.hasSections() &&
1093
+ doc.hasAttribute('toc')
1094
+ ) {
1095
+ toc = `
1096
+ <div id="toc" class="${doc.getAttribute('toc-class', 'toc')}">
1097
+ <div id="toctitle">${doc.getAttribute('toc-title')}</div>
1098
+ ${await doc.converter.convert(doc, 'outline')}
1099
+ </div>`
1100
+ }
1101
+ return `<div id="preamble">
1102
+ <div class="sectionbody">
1103
+ ${await node.content()}
1104
+ </div>${toc}
1105
+ </div>`
1106
+ }
1107
+
1108
+ async convert_quote(node) {
1109
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1110
+ const classes = ['quoteblock', node.role].filter(Boolean)
1111
+ const classAttribute = ` class="${classes.join(' ')}"`
1112
+ const titleElement = node.hasTitle()
1113
+ ? `\n<div class="title">${node.title}</div>`
1114
+ : ''
1115
+ const attribution = node.hasAttribute('attribution')
1116
+ ? node.getAttribute('attribution')
1117
+ : null
1118
+ const citetitle = node.hasAttribute('citetitle')
1119
+ ? node.getAttribute('citetitle')
1120
+ : null
1121
+ let attributionElement = ''
1122
+ if (attribution || citetitle) {
1123
+ const citeElement = citetitle ? `<cite>${citetitle}</cite>` : ''
1124
+ const attributionText = attribution
1125
+ ? `&#8212; ${attribution}${citetitle ? `<br${this._voidSlash}>\n` : ''}`
1126
+ : ''
1127
+ attributionElement = `\n<div class="attribution">\n${attributionText}${citeElement}\n</div>`
1128
+ }
1129
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1130
+ <blockquote>
1131
+ ${await node.content()}
1132
+ </blockquote>${attributionElement}
1133
+ </div>`
1134
+ }
1135
+
1136
+ async convert_thematic_break(node) {
1137
+ const classAttribute = node.role ? ` class="${node.role}"` : ''
1138
+ return `<hr${classAttribute}${this._voidSlash}>`
1139
+ }
1140
+
1141
+ async convert_sidebar(node) {
1142
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1143
+ const titleElement = node.hasTitle()
1144
+ ? `<div class="title">${node.title}</div>\n`
1145
+ : ''
1146
+ const role = node.role
1147
+ return `<div${idAttribute} class="sidebarblock${role ? ` ${role}` : ''}">
1148
+ <div class="content">
1149
+ ${titleElement}${await node.content()}
1150
+ </div>
1151
+ </div>`
1152
+ }
1153
+
1154
+ async convert_table(node) {
1155
+ const result = []
1156
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1157
+ let frame = node.getAttribute('frame', 'all', 'table-frame')
1158
+ if (frame === 'topbot') frame = 'ends'
1159
+ const classes = [
1160
+ 'tableblock',
1161
+ `frame-${frame}`,
1162
+ `grid-${node.getAttribute('grid', 'all', 'table-grid')}`,
1163
+ ]
1164
+ const stripes = node.getAttribute('stripes', null, 'table-stripes')
1165
+ if (stripes) classes.push(`stripes-${stripes}`)
1166
+ let widthAttribute = ''
1167
+ const autowidth = node.hasOption('autowidth')
1168
+ if (autowidth && !node.hasAttribute('width')) {
1169
+ classes.push('fit-content')
1170
+ } else {
1171
+ const tablewidth = node.getAttribute('tablepcwidth')
1172
+ if (Number(tablewidth) === 100) {
1173
+ classes.push('stretch')
1174
+ } else {
1175
+ widthAttribute = ` width="${tablewidth}%"`
1176
+ }
1177
+ }
1178
+ if (node.hasAttribute('float')) classes.push(node.getAttribute('float'))
1179
+ if (node.role) classes.push(node.role)
1180
+ const classAttribute = ` class="${classes.join(' ')}"`
1181
+
1182
+ result.push(`<table${idAttribute}${classAttribute}${widthAttribute}>`)
1183
+ if (node.hasTitle())
1184
+ result.push(`<caption class="title">${node.captionedTitle()}</caption>`)
1185
+
1186
+ if (node.getAttribute('rowcount') > 0) {
1187
+ const slash = this._voidSlash
1188
+ result.push('<colgroup>')
1189
+ if (autowidth) {
1190
+ for (let i = 0; i < node.columns.length; i++)
1191
+ result.push(`<col${slash}>`)
1192
+ } else {
1193
+ for (const col of node.columns) {
1194
+ result.push(
1195
+ col.hasOption('autowidth')
1196
+ ? `<col${slash}>`
1197
+ : `<col width="${col.getAttribute('colpcwidth')}%"${slash}>`
1198
+ )
1199
+ }
1200
+ }
1201
+ result.push('</colgroup>')
1202
+
1203
+ for (const [tsec, rows] of node.rows.bySection()) {
1204
+ if (rows.length === 0) continue
1205
+ result.push(`<t${tsec}>`)
1206
+ for (const row of rows) {
1207
+ result.push('<tr>')
1208
+ for (const cell of row) {
1209
+ let cellContent
1210
+ if (tsec === 'head') {
1211
+ cellContent = cell.text
1212
+ } else {
1213
+ switch (cell.style) {
1214
+ case 'asciidoc':
1215
+ cellContent = `<div class="content">${await cell.content()}</div>`
1216
+ break
1217
+ case 'literal':
1218
+ cellContent = `<div class="literal"><pre>${cell.text}</pre></div>`
1219
+ break
1220
+ default: {
1221
+ const parts = await cell.content()
1222
+ cellContent =
1223
+ parts.length === 0
1224
+ ? ''
1225
+ : `<p class="tableblock">${parts.join('</p>\n<p class="tableblock">')}</p>`
1226
+ }
1227
+ }
1228
+ }
1229
+ const cellTagName =
1230
+ tsec === 'head' || cell.style === 'header' ? 'th' : 'td'
1231
+ const cellClassAttr = ` class="tableblock halign-${cell.getAttribute('halign')} valign-${cell.getAttribute('valign')}"`
1232
+ const cellColspanAttr = cell.colspan
1233
+ ? ` colspan="${cell.colspan}"`
1234
+ : ''
1235
+ const cellRowspanAttr = cell.rowspan
1236
+ ? ` rowspan="${cell.rowspan}"`
1237
+ : ''
1238
+ // Use the per-cell captured cellbgcolor (set by {set:cellbgcolor:...} in cell text
1239
+ // during precomputeText). Fall back to the current document attribute if not captured.
1240
+ const cellbgcolor =
1241
+ '_cellbgcolor' in cell
1242
+ ? cell._cellbgcolor
1243
+ : node.document.attributes.cellbgcolor
1244
+ const cellStyleAttr = cellbgcolor
1245
+ ? ` style="background-color: ${cellbgcolor};"`
1246
+ : ''
1247
+ result.push(
1248
+ `<${cellTagName}${cellClassAttr}${cellColspanAttr}${cellRowspanAttr}${cellStyleAttr}>${cellContent}</${cellTagName}>`
1249
+ )
1250
+ }
1251
+ result.push('</tr>')
1252
+ }
1253
+ result.push(`</t${tsec}>`)
1254
+ }
1255
+ }
1256
+ result.push('</table>')
1257
+ return result.join(LF)
1258
+ }
1259
+
1260
+ async convert_toc(node) {
1261
+ const doc = node.document
1262
+ if (
1263
+ !doc.hasAttribute('toc-placement', 'macro') ||
1264
+ !doc.hasSections() ||
1265
+ !doc.hasAttribute('toc')
1266
+ ) {
1267
+ return '<!-- toc disabled -->'
1268
+ }
1269
+ let idAttr, titleIdAttr
1270
+ if (node.id) {
1271
+ idAttr = ` id="${node.id}"`
1272
+ titleIdAttr = ` id="${node.id}title"`
1273
+ } else {
1274
+ idAttr = ' id="toc"'
1275
+ titleIdAttr = ' id="toctitle"'
1276
+ }
1277
+ const title = node.hasTitle() ? node.title : doc.getAttribute('toc-title')
1278
+ const levels = node.hasAttribute('levels')
1279
+ ? parseInt(node.getAttribute('levels'), 10)
1280
+ : null
1281
+ const role = node.hasRoleAttribute()
1282
+ ? node.role
1283
+ : doc.getAttribute('toc-class', 'toc')
1284
+ return `<div${idAttr} class="${role}">
1285
+ <div${titleIdAttr} class="title">${title}</div>
1286
+ ${await doc.converter.convert(doc, 'outline', levels != null ? { toclevels: levels } : {})}
1287
+ </div>`
1288
+ }
1289
+
1290
+ async convert_ulist(node) {
1291
+ const result = []
1292
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1293
+ const divClasses = ['ulist', node.style, node.role].filter(Boolean)
1294
+ let markerChecked = ''
1295
+ let markerUnchecked = ''
1296
+ let ulClassAttribute
1297
+ const checklist = node.hasOption('checklist')
1298
+ if (checklist) {
1299
+ divClasses.splice(1, 0, 'checklist')
1300
+ ulClassAttribute = ' class="checklist"'
1301
+ if (node.hasOption('interactive')) {
1302
+ if (this._xmlMode) {
1303
+ markerChecked =
1304
+ '<input type="checkbox" data-item-complete="1" checked="checked"/> '
1305
+ markerUnchecked = '<input type="checkbox" data-item-complete="0"/> '
1306
+ } else {
1307
+ markerChecked =
1308
+ '<input type="checkbox" data-item-complete="1" checked> '
1309
+ markerUnchecked = '<input type="checkbox" data-item-complete="0"> '
1310
+ }
1311
+ } else if (node.document.hasAttribute('icons', 'font')) {
1312
+ markerChecked = '<i class="fa fa-check-square-o"></i> '
1313
+ markerUnchecked = '<i class="fa fa-square-o"></i> '
1314
+ } else {
1315
+ markerChecked = '&#10003; '
1316
+ markerUnchecked = '&#10063; '
1317
+ }
1318
+ } else {
1319
+ ulClassAttribute = node.style ? ` class="${node.style}"` : ''
1320
+ }
1321
+ result.push(`<div${idAttribute} class="${divClasses.join(' ')}">`)
1322
+ if (node.hasTitle()) result.push(`<div class="title">${node.title}</div>`)
1323
+ result.push(`<ul${ulClassAttribute}>`)
1324
+
1325
+ for (const item of node.getItems()) {
1326
+ if (item.id) {
1327
+ result.push(
1328
+ `<li id="${item.id}"${item.role ? ` class="${item.role}"` : ''}>`
1329
+ )
1330
+ } else if (item.role) {
1331
+ result.push(`<li class="${item.role}">`)
1332
+ } else {
1333
+ result.push('<li>')
1334
+ }
1335
+ if (checklist && item.hasAttribute('checkbox')) {
1336
+ result.push(
1337
+ `<p>${item.hasAttribute('checked') ? markerChecked : markerUnchecked}${item.getText()}</p>`
1338
+ )
1339
+ } else {
1340
+ result.push(`<p>${item.getText()}</p>`)
1341
+ }
1342
+ if (item.hasBlocks()) result.push(await item.content())
1343
+ result.push('</li>')
1344
+ }
1345
+
1346
+ result.push('</ul>')
1347
+ result.push('</div>')
1348
+ return result.join(LF)
1349
+ }
1350
+
1351
+ async convert_verse(node) {
1352
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1353
+ const classes = ['verseblock', node.role].filter(Boolean)
1354
+ const classAttribute = ` class="${classes.join(' ')}"`
1355
+ const titleElement = node.hasTitle()
1356
+ ? `\n<div class="title">${node.title}</div>`
1357
+ : ''
1358
+ const attribution = node.hasAttribute('attribution')
1359
+ ? node.getAttribute('attribution')
1360
+ : null
1361
+ const citetitle = node.hasAttribute('citetitle')
1362
+ ? node.getAttribute('citetitle')
1363
+ : null
1364
+ let attributionElement = ''
1365
+ if (attribution || citetitle) {
1366
+ const citeElement = citetitle ? `<cite>${citetitle}</cite>` : ''
1367
+ const attributionText = attribution
1368
+ ? `&#8212; ${attribution}${citetitle ? `<br${this._voidSlash}>\n` : ''}`
1369
+ : ''
1370
+ attributionElement = `\n<div class="attribution">\n${attributionText}${citeElement}\n</div>`
1371
+ }
1372
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1373
+ <pre class="content">${await node.content()}</pre>${attributionElement}
1374
+ </div>`
1375
+ }
1376
+
1377
+ async convert_video(node) {
1378
+ const xml = this._xmlMode
1379
+ const idAttribute = node.id ? ` id="${node.id}"` : ''
1380
+ const classes = ['videoblock']
1381
+ if (node.hasAttribute('float')) classes.push(node.getAttribute('float'))
1382
+ if (node.hasAttribute('align'))
1383
+ classes.push(`text-${node.getAttribute('align')}`)
1384
+ if (node.role) classes.push(node.role)
1385
+ const classAttribute = ` class="${classes.join(' ')}"`
1386
+ const titleElement = node.hasTitle()
1387
+ ? `\n<div class="title">${node.title}</div>`
1388
+ : ''
1389
+ const widthAttribute = node.hasAttribute('width')
1390
+ ? ` width="${node.getAttribute('width')}"`
1391
+ : ''
1392
+ const heightAttribute = node.hasAttribute('height')
1393
+ ? ` height="${node.getAttribute('height')}"`
1394
+ : ''
1395
+
1396
+ switch (node.getAttribute('poster')) {
1397
+ case 'vimeo': {
1398
+ let assetUriScheme = node.document.getAttribute(
1399
+ 'asset-uri-scheme',
1400
+ 'https'
1401
+ )
1402
+ if (assetUriScheme) assetUriScheme = `${assetUriScheme}:`
1403
+ const startAnchor = node.hasAttribute('start')
1404
+ ? `#at=${node.getAttribute('start')}`
1405
+ : ''
1406
+ const delimiter = ['?']
1407
+ let [target, hash] = node.getAttribute('target').split('/', 2)
1408
+ hash ||= node.getAttribute('hash')
1409
+ const hashParam = hash ? `${delimiter.pop() || '&amp;'}h=${hash}` : ''
1410
+ const autoplayParam = node.hasOption('autoplay')
1411
+ ? `${delimiter.pop() || '&amp;'}autoplay=1`
1412
+ : ''
1413
+ const loopParam = node.hasOption('loop')
1414
+ ? `${delimiter.pop() || '&amp;'}loop=1`
1415
+ : ''
1416
+ const mutedParam = node.hasOption('muted')
1417
+ ? `${delimiter.pop() || '&amp;'}muted=1`
1418
+ : ''
1419
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1420
+ <div class="content">
1421
+ <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>
1422
+ </div>
1423
+ </div>`
1424
+ }
1425
+ case 'youtube': {
1426
+ let assetUriScheme = node.document.getAttribute(
1427
+ 'asset-uri-scheme',
1428
+ 'https'
1429
+ )
1430
+ if (assetUriScheme) assetUriScheme = `${assetUriScheme}:`
1431
+ const relParamVal = node.hasOption('related') ? 1 : 0
1432
+ const startParam = node.hasAttribute('start')
1433
+ ? `&amp;start=${node.getAttribute('start')}`
1434
+ : ''
1435
+ const endParam = node.hasAttribute('end')
1436
+ ? `&amp;end=${node.getAttribute('end')}`
1437
+ : ''
1438
+ const autoplayParam = node.hasOption('autoplay')
1439
+ ? '&amp;autoplay=1'
1440
+ : ''
1441
+ const hasLoopParam = node.hasOption('loop')
1442
+ const loopParam = hasLoopParam ? '&amp;loop=1' : ''
1443
+ const muteParam = node.hasOption('muted') ? '&amp;mute=1' : ''
1444
+ const controlsParam = node.hasOption('nocontrols')
1445
+ ? '&amp;controls=0'
1446
+ : ''
1447
+ let fsParam, fsAttribute
1448
+ if (node.hasOption('nofullscreen')) {
1449
+ fsParam = '&amp;fs=0'
1450
+ fsAttribute = ''
1451
+ } else {
1452
+ fsParam = ''
1453
+ fsAttribute = this._appendBooleanAttr('allowfullscreen', xml)
1454
+ }
1455
+ const modestParam = node.hasOption('modest')
1456
+ ? '&amp;modestbranding=1'
1457
+ : ''
1458
+ const themeParam = node.hasAttribute('theme')
1459
+ ? `&amp;theme=${node.getAttribute('theme')}`
1460
+ : ''
1461
+ const hlParam = node.hasAttribute('lang')
1462
+ ? `&amp;hl=${node.getAttribute('lang')}`
1463
+ : ''
1464
+ let [target, list] = node.getAttribute('target').split('/', 2)
1465
+ list ||= node.getAttribute('list')
1466
+ let listParam
1467
+ if (list) {
1468
+ listParam = `&amp;list=${list}`
1469
+ } else {
1470
+ let playlist
1471
+ const videoParts = target.split(',')
1472
+ target = videoParts[0]
1473
+ playlist =
1474
+ videoParts.length > 1 ? videoParts.slice(1).join(',') : null
1475
+ playlist ||= node.getAttribute('playlist')
1476
+ if (playlist) {
1477
+ listParam = `&amp;playlist=${target},${playlist}`
1478
+ } else {
1479
+ listParam = hasLoopParam ? `&amp;playlist=${target}` : ''
1480
+ }
1481
+ }
1482
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1483
+ <div class="content">
1484
+ <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>
1485
+ </div>
1486
+ </div>`
1487
+ }
1488
+ case 'wistia': {
1489
+ let assetUriScheme = node.document.getAttribute(
1490
+ 'asset-uri-scheme',
1491
+ 'https'
1492
+ )
1493
+ if (assetUriScheme) assetUriScheme = `${assetUriScheme}:`
1494
+ const delimiter = ['?']
1495
+ const startAnchor = node.hasAttribute('start')
1496
+ ? `${delimiter.pop() || '&amp;'}time=${node.getAttribute('start')}`
1497
+ : ''
1498
+ const endVideoBehaviorParam = node.hasOption('loop')
1499
+ ? `${delimiter.pop() || '&amp;'}endVideoBehavior=loop`
1500
+ : node.hasOption('reset')
1501
+ ? `${delimiter.pop() || '&amp;'}endVideoBehavior=reset`
1502
+ : ''
1503
+ const target = node.getAttribute('target')
1504
+ const autoplayParam = node.hasOption('autoplay')
1505
+ ? `${delimiter.pop() || '&amp;'}autoPlay=true`
1506
+ : ''
1507
+ const mutedParam = node.hasOption('muted')
1508
+ ? `${delimiter.pop() || '&amp;'}muted=true`
1509
+ : ''
1510
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1511
+ <div class="content">
1512
+ <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>
1513
+ </div>
1514
+ </div>`
1515
+ }
1516
+ default: {
1517
+ const posterVal = node.getAttribute('poster')
1518
+ const posterAttribute = !posterVal
1519
+ ? ''
1520
+ : ` poster="${node.mediaUri(posterVal)}"`
1521
+ const preloadVal = node.getAttribute('preload')
1522
+ const preloadAttribute = !preloadVal ? '' : ` preload="${preloadVal}"`
1523
+ const startT = node.getAttribute('start')
1524
+ const endT = node.getAttribute('end')
1525
+ const timeAnchor =
1526
+ startT || endT ? `#t=${startT || ''}${endT ? `,${endT}` : ''}` : ''
1527
+ return `<div${idAttribute}${classAttribute}>${titleElement}
1528
+ <div class="content">
1529
+ <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}>
1530
+ Your browser does not support the video tag.
1531
+ </video>
1532
+ </div>
1533
+ </div>`
1534
+ }
1535
+ }
1536
+ }
1537
+
1538
+ async convert_inline_anchor(node) {
1539
+ switch (node.type) {
1540
+ case 'xref': {
1541
+ let attrs, text
1542
+ if (node.attributes.path) {
1543
+ attrs = this._appendLinkConstraintAttrs(
1544
+ node,
1545
+ node.role ? [` class="${node.role}"`] : []
1546
+ ).join('')
1547
+ text = node.text || node.attributes.path
1548
+ } else {
1549
+ attrs = node.role ? ` class="${node.role}"` : ''
1550
+ if (!(text = node.text)) {
1551
+ const refs = (this._refs ??= node.document.catalog.refs)
1552
+ const refid = node.attributes.refid
1553
+ let top
1554
+ const ref =
1555
+ refs[refid] ??
1556
+ (!refid ? (top = this._getRootDocument(node)) : null)
1557
+ if (ref instanceof AbstractNode) {
1558
+ const resolvingSet = (this._resolvingXrefs ??= new Set())
1559
+ if (!resolvingSet.has(refid)) {
1560
+ resolvingSet.add(refid)
1561
+ const resolved = await ref.xreftext(
1562
+ node.getAttribute('xrefstyle', null, true)
1563
+ )
1564
+ resolvingSet.delete(refid)
1565
+ if (resolved) {
1566
+ text = resolved.includes('<a')
1567
+ ? resolved.replace(new RegExp(DropAnchorRx.source, 'g'), '')
1568
+ : resolved
1569
+ } else {
1570
+ text = top ? '[^top]' : `[${refid}]`
1571
+ }
1572
+ } else {
1573
+ text = top ? '[^top]' : `[${refid}]`
1574
+ }
1575
+ } else {
1576
+ text = `[${refid}]`
1577
+ }
1578
+ }
1579
+ }
1580
+ return `<a href="${node.target}"${attrs}>${text}</a>`
1581
+ }
1582
+ case 'ref':
1583
+ return `<a id="${node.id}"></a>`
1584
+ case 'link': {
1585
+ const attrs = node.id ? [` id="${node.id}"`] : []
1586
+ if (node.role) attrs.push(` class="${node.role}"`)
1587
+ if (node.hasAttribute('title'))
1588
+ attrs.push(` title="${node.getAttribute('title')}"`)
1589
+ return `<a href="${node.target}"${this._appendLinkConstraintAttrs(node, attrs).join('')}>${node.text ?? ''}</a>`
1590
+ }
1591
+ case 'bibref':
1592
+ return `<a id="${node.id}"></a>[${node.reftext || node.id}]`
1593
+ default:
1594
+ this.logger.warn(`unknown anchor type: ${node.type}`)
1595
+ return null
1596
+ }
1597
+ }
1598
+
1599
+ async convert_inline_break(node) {
1600
+ return `${node.text}<br${this._voidSlash}>`
1601
+ }
1602
+
1603
+ async convert_inline_button(node) {
1604
+ return `<b class="button">${node.text}</b>`
1605
+ }
1606
+
1607
+ async convert_inline_callout(node) {
1608
+ if (node.document.hasAttribute('icons', 'font')) {
1609
+ return `<i class="conum" data-value="${node.text}"></i><b>(${node.text})</b>`
1610
+ }
1611
+ if (node.document.hasAttribute('icons')) {
1612
+ const src = await node.iconUri(`callouts/${node.text}`)
1613
+ return `<img src="${src}" alt="${node.text}"${this._voidSlash}>`
1614
+ }
1615
+ const guard = node.attributes.guard
1616
+ if (Array.isArray(guard)) {
1617
+ return `&lt;!--<b class="conum">(${node.text})</b>--&gt;`
1618
+ }
1619
+ return `${guard ?? ''}<b class="conum">(${node.text})</b>`
1620
+ }
1621
+
1622
+ async convert_inline_footnote(node) {
1623
+ const index = node.getAttribute('index')
1624
+ if (index) {
1625
+ if (node.type === 'xref') {
1626
+ return `<sup class="footnoteref">[<a class="footnote" href="#_footnotedef_${index}" title="View footnote.">${index}</a>]</sup>`
1627
+ }
1628
+ const idAttr = node.id ? ` id="_footnote_${node.id}"` : ''
1629
+ return `<sup class="footnote"${idAttr}>[<a id="_footnoteref_${index}" class="footnote" href="#_footnotedef_${index}" title="View footnote.">${index}</a>]</sup>`
1630
+ }
1631
+ if (node.type === 'xref') {
1632
+ return `<sup class="footnoteref red" title="Unresolved footnote reference.">[${node.text}]</sup>`
1633
+ }
1634
+ return null
1635
+ }
1636
+
1637
+ async convert_inline_image(node) {
1638
+ const target = node.target
1639
+ const type = node.type || 'image'
1640
+ let img, src
1641
+ if (type === 'icon') {
1642
+ const icons = node.document.getAttribute('icons')
1643
+ if (icons === 'font') {
1644
+ let iClassAttrVal = `fa fa-${target}`
1645
+ if (node.hasAttribute('size'))
1646
+ iClassAttrVal += ` fa-${node.getAttribute('size')}`
1647
+ if (node.hasAttribute('flip')) {
1648
+ iClassAttrVal += ` fa-flip-${node.getAttribute('flip')}`
1649
+ } else if (node.hasAttribute('rotate')) {
1650
+ iClassAttrVal += ` fa-rotate-${node.getAttribute('rotate')}`
1651
+ }
1652
+ const titleAttr = node.hasAttribute('title')
1653
+ ? ` title="${node.getAttribute('title')}"`
1654
+ : ''
1655
+ img = `<i class="${iClassAttrVal}"${titleAttr}></i>`
1656
+ } else if (icons != null) {
1657
+ let attrs = node.hasAttribute('width')
1658
+ ? ` width="${node.getAttribute('width')}"`
1659
+ : ''
1660
+ if (node.hasAttribute('height'))
1661
+ attrs += ` height="${node.getAttribute('height')}"`
1662
+ if (node.hasAttribute('title'))
1663
+ attrs += ` title="${node.getAttribute('title')}"`
1664
+ img = `<img src="${await node.iconUri(target)}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1665
+ } else {
1666
+ img = `[${node.getAlt()}&#93;`
1667
+ }
1668
+ } else {
1669
+ let attrs = node.hasAttribute('width')
1670
+ ? ` width="${node.getAttribute('width')}"`
1671
+ : ''
1672
+ if (node.hasAttribute('height'))
1673
+ attrs += ` height="${node.getAttribute('height')}"`
1674
+ if (node.hasAttribute('title'))
1675
+ attrs += ` title="${node.getAttribute('title')}"`
1676
+ if (
1677
+ (node.hasAttribute('format', 'svg') || target.includes('.svg')) &&
1678
+ node.document.safe < SafeMode.SECURE
1679
+ ) {
1680
+ if (node.hasOption('inline')) {
1681
+ img =
1682
+ (await this.readSvgContents(node, target)) ||
1683
+ `<span class="alt">${node.getAlt()}</span>`
1684
+ } else if (node.hasOption('interactive')) {
1685
+ const fallback = node.hasAttribute('fallback')
1686
+ ? `<img src="${await node.imageUri(node.getAttribute('fallback'))}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1687
+ : `<span class="alt">${node.getAlt()}</span>`
1688
+ src = await node.imageUri(target)
1689
+ img = `<object type="image/svg+xml" data="${src}"${attrs}>${fallback}</object>`
1690
+ } else {
1691
+ src = await node.imageUri(target)
1692
+ img = `<img src="${src}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1693
+ }
1694
+ } else {
1695
+ src = await node.imageUri(target)
1696
+ img = `<img src="${src}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1697
+ }
1698
+ }
1699
+
1700
+ if (node.hasAttribute('link')) {
1701
+ let hrefAttrVal = node.getAttribute('link')
1702
+ if (hrefAttrVal === 'self') hrefAttrVal = src
1703
+ if (hrefAttrVal) {
1704
+ img = `<a class="image" href="${hrefAttrVal}"${this._appendLinkConstraintAttrs(node).join('')}>${img}</a>`
1705
+ }
1706
+ }
1707
+
1708
+ const idAttr = node.id ? ` id="${node.id}"` : ''
1709
+ let classAttrVal = type
1710
+ const role = node.role
1711
+ if (role) {
1712
+ classAttrVal = node.hasAttribute('float')
1713
+ ? `${classAttrVal} ${node.getAttribute('float')} ${role}`
1714
+ : `${classAttrVal} ${role}`
1715
+ } else if (node.hasAttribute('float')) {
1716
+ classAttrVal = `${classAttrVal} ${node.getAttribute('float')}`
1717
+ }
1718
+ return `<span${idAttr} class="${classAttrVal}">${img}</span>`
1719
+ }
1720
+
1721
+ async convert_inline_indexterm(node) {
1722
+ return node.type === 'visible' ? node.text : ''
1723
+ }
1724
+
1725
+ async convert_inline_kbd(node) {
1726
+ const keys = node.getAttribute('keys')
1727
+ if (keys.length === 1) {
1728
+ return `<kbd>${keys[0]}</kbd>`
1729
+ }
1730
+ return `<span class="keyseq"><kbd>${keys.join('</kbd>+<kbd>')}</kbd></span>`
1731
+ }
1732
+
1733
+ async convert_inline_menu(node) {
1734
+ const caret = node.document.hasAttribute('icons', 'font')
1735
+ ? '&#160;<i class="fa fa-angle-right caret"></i> '
1736
+ : '&#160;<b class="caret">&#8250;</b> '
1737
+ const submenuJoiner = `</b>${caret}<b class="submenu">`
1738
+ const menu = node.getAttribute('menu')
1739
+ const submenus = node.getAttribute('submenus')
1740
+ if (!submenus || submenus.length === 0) {
1741
+ const menuitem = node.getAttribute('menuitem')
1742
+ if (menuitem) {
1743
+ return `<span class="menuseq"><b class="menu">${menu}</b>${caret}<b class="menuitem">${menuitem}</b></span>`
1744
+ }
1745
+ return `<b class="menuref">${menu}</b>`
1746
+ }
1747
+ 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>`
1748
+ }
1749
+
1750
+ async convert_inline_quoted(node) {
1751
+ const [open, close, tag] = QUOTE_TAGS[node.type] ?? DEFAULT_QUOTE_TAG
1752
+ if (node.id) {
1753
+ const classAttr = node.role ? ` class="${node.role}"` : ''
1754
+ if (tag) {
1755
+ return `${open.slice(0, -1)} id="${node.id}"${classAttr}>${node.text}${close}`
1756
+ }
1757
+ return `<span id="${node.id}"${classAttr}>${open}${node.text}${close}</span>`
1758
+ }
1759
+ if (node.role) {
1760
+ if (tag) {
1761
+ return `${open.slice(0, -1)} class="${node.role}">${node.text}${close}`
1762
+ }
1763
+ return `<span class="${node.role}">${open}${node.text}${close}</span>`
1764
+ }
1765
+ return `${open}${node.text}${close}`
1766
+ }
1767
+
1768
+ // NOTE expose readSvgContents for Bespoke converter
1769
+ async readSvgContents(node, target) {
1770
+ const imagesdir = node.document.getAttribute('imagesdir')
1771
+ let resolvedPath
1772
+ let svg
1773
+ if (isUriish(target) || (imagesdir && isUriish(imagesdir))) {
1774
+ svg = await node.readContents(target, {
1775
+ start: imagesdir,
1776
+ normalize: true,
1777
+ warnOnFailure: true,
1778
+ label: 'SVG',
1779
+ })
1780
+ resolvedPath = target
1781
+ } else {
1782
+ resolvedPath = node.normalizeSystemPath(target, imagesdir, null, {
1783
+ targetName: 'image',
1784
+ })
1785
+ svg = await node.readAsset(resolvedPath, {
1786
+ normalize: true,
1787
+ warnOnFailure: true,
1788
+ label: 'SVG',
1789
+ })
1790
+ }
1791
+ if (svg == null) return null // file not found/readable; warning already emitted
1792
+ if (!svg) {
1793
+ node.logger.warn(`contents of SVG is empty: ${resolvedPath}`)
1794
+ return null
1795
+ }
1796
+ if (!svg.startsWith('<svg')) svg = svg.replace(SvgPreambleRx, '')
1797
+ // Fix incomplete SVG start tag (missing closing >) by inserting > before the first child element.
1798
+ // This handles cases like: <svg width="500"\n<circle .../> where the > is missing.
1799
+ svg = svg.replace(
1800
+ /^(<svg\b[^<>]*?)(\s*<[^/!])/s,
1801
+ (_, pre, rest) => `${pre.trimEnd()}>${rest}`
1802
+ )
1803
+ let oldStartTag = null
1804
+ let newStartTag = null
1805
+ let startTagMatch = null
1806
+ for (const dim of ['width', 'height']) {
1807
+ if (!node.hasAttribute(dim)) continue
1808
+ if (!newStartTag) {
1809
+ if (startTagMatch === null)
1810
+ startTagMatch = svg.match(SvgStartTagRx) || false
1811
+ if (!startTagMatch) continue
1812
+ oldStartTag = startTagMatch[0]
1813
+ newStartTag = oldStartTag.replace(
1814
+ new RegExp(DimensionAttributeRx.source, 'g'),
1815
+ ''
1816
+ )
1817
+ }
1818
+ newStartTag = `${newStartTag.slice(0, -1)} ${dim}="${node.getAttribute(dim)}">`
1819
+ }
1820
+ if (newStartTag) svg = `${newStartTag}${svg.slice(oldStartTag.length)}`
1821
+ return svg
1822
+ }
1823
+
1824
+ // ── Private helpers ─────────────────────────────────────────────────────────
1825
+
1826
+ /**
1827
+ * @internal
1828
+ * @private
1829
+ */
1830
+ _appendBooleanAttr(name, xml) {
1831
+ return xml ? ` ${name}="${name}"` : ` ${name}`
1832
+ }
1833
+
1834
+ /**
1835
+ * @internal
1836
+ * @private
1837
+ */
1838
+ _appendLinkConstraintAttrs(node, attrs = []) {
1839
+ const rel = node.hasOption('nofollow') ? 'nofollow' : null
1840
+ const window = node.attributes.window
1841
+ if (window) {
1842
+ attrs.push(` target="${window}"`)
1843
+ if (window === '_blank' || node.hasOption('noopener')) {
1844
+ attrs.push(rel ? ` rel="${rel} noopener"` : ' rel="noopener"')
1845
+ }
1846
+ } else if (rel) {
1847
+ attrs.push(` rel="${rel}"`)
1848
+ }
1849
+ return attrs
1850
+ }
1851
+
1852
+ /**
1853
+ * @internal
1854
+ * @private
1855
+ */
1856
+ _encodeAttrValue(val) {
1857
+ return val.includes('"') ? val.replace(/"/g, '&quot;') : val
1858
+ }
1859
+
1860
+ /**
1861
+ * @internal
1862
+ * @private
1863
+ */
1864
+ _generateMannameSection(node) {
1865
+ let mannameTitle = node.getAttribute('manname-title', 'Name')
1866
+ const sections = node.sections()
1867
+ if (sections.length > 0) {
1868
+ const nextSectionTitle = sections[0].title
1869
+ if (nextSectionTitle === nextSectionTitle.toUpperCase()) {
1870
+ mannameTitle = mannameTitle.toUpperCase()
1871
+ }
1872
+ }
1873
+ const mannameId = node.getAttribute('manname-id')
1874
+ const mannameIdAttr = mannameId ? ` id="${mannameId}"` : ''
1875
+ return `<h2${mannameIdAttr}>${mannameTitle}</h2>
1876
+ <div class="sectionbody">
1877
+ <p>${node.getAttribute('mannames').join(', ')} - ${node.getAttribute('manpurpose')}</p>
1878
+ </div>`
1879
+ }
1880
+
1881
+ /**
1882
+ * @internal
1883
+ * @private
1884
+ */
1885
+ _getRootDocument(node) {
1886
+ while ((node = node.document).isNested()) {
1887
+ node = node.parentDocument
1888
+ }
1889
+ return node
1890
+ }
1891
+ }
1892
+
1893
+ Html5Converter.registerFor('html5')