@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,2241 @@
1
+ // ESM conversion of substitutors.rb
2
+ // This module is intended to be mixed into Section and Block via Object.assign(Target.prototype, Substitutors)
3
+
4
+ import {
5
+ LF,
6
+ ATTR_REF_HEAD,
7
+ HARD_LINE_BREAK,
8
+ INTRINSIC_ATTRIBUTES,
9
+ REPLACEMENTS,
10
+ ReplaceableTextRx,
11
+ QUOTE_SUBS,
12
+ InlinePassMacroRx,
13
+ InlinePassRx,
14
+ InlineStemMacroRx,
15
+ STEM_TYPE_ALIASES,
16
+ InlineKbdBtnMacroRx,
17
+ InlineMenuMacroRx,
18
+ InlineMenuRx,
19
+ InlineImageMacroRx,
20
+ InlineIndextermMacroRx,
21
+ InlineLinkRx,
22
+ UriSniffRx,
23
+ InlineLinkMacroRx,
24
+ InlineEmailRx,
25
+ InlineBiblioAnchorRx,
26
+ InlineAnchorRx,
27
+ InlineXrefMacroRx,
28
+ InlineFootnoteMacroRx,
29
+ HardLineBreakRx,
30
+ CalloutSourceRxMap,
31
+ CalloutSourceRx,
32
+ CalloutExtractRxMap,
33
+ CalloutExtractRx,
34
+ SubModifierSniffRx,
35
+ ASCIIDOC_EXTENSIONS,
36
+ AttributeReferenceRx,
37
+ BASIC_SUBS,
38
+ NORMAL_SUBS,
39
+ } from './constants.js'
40
+ import { Compliance } from './compliance.js'
41
+ import { Parser } from './parser.js'
42
+ import {
43
+ asyncReplace,
44
+ basename,
45
+ encodeUriComponent,
46
+ isExtname,
47
+ } from './helpers.js'
48
+ import { Inline } from './inline.js'
49
+ import { AttributeList } from './attribute_list.js'
50
+ import { Footnote } from './footnote.js'
51
+
52
+ // ── Module-level constants ────────────────────────────────────────────────────
53
+
54
+ const SPECIAL_CHARS_RX = /[<&>]/g
55
+ const SPECIAL_CHARS_TR = { '>': '&gt;', '<': '&lt;', '&': '&amp;' }
56
+
57
+ // Detects if text is a possible candidate for the quotes substitution.
58
+ const QUOTED_TEXT_SNIFF_RX = {
59
+ false: /[*_`#^~]/,
60
+ true: /[*'_+#^~]/,
61
+ }
62
+
63
+ const HEADER_SUBS = Object.freeze(['specialcharacters', 'attributes'])
64
+ const NO_SUBS = Object.freeze([])
65
+ const REFTEXT_SUBS = Object.freeze([
66
+ 'specialcharacters',
67
+ 'quotes',
68
+ 'replacements',
69
+ ])
70
+ const VERBATIM_SUBS = Object.freeze(['specialcharacters', 'callouts'])
71
+
72
+ const SUB_GROUPS = {
73
+ none: NO_SUBS,
74
+ normal: NORMAL_SUBS,
75
+ verbatim: VERBATIM_SUBS,
76
+ specialchars: BASIC_SUBS,
77
+ }
78
+
79
+ const SUB_HINTS = {
80
+ a: 'attributes',
81
+ m: 'macros',
82
+ n: 'normal',
83
+ p: 'post_replacements',
84
+ q: 'quotes',
85
+ r: 'replacements',
86
+ c: 'specialcharacters',
87
+ v: 'verbatim',
88
+ }
89
+
90
+ const SUB_OPTIONS = {
91
+ block: [...Object.keys(SUB_GROUPS), ...NORMAL_SUBS, 'callouts'],
92
+ inline: [...Object.keys(SUB_GROUPS), ...NORMAL_SUBS],
93
+ }
94
+
95
+ // control characters used as placeholders
96
+ const CAN = '\u0018'
97
+ const DEL = '\u007f'
98
+
99
+ // SPA, start of guarded protected area (\u0096)
100
+ const PASS_START = '\u0096'
101
+
102
+ // EPA, end of guarded protected area (\u0097)
103
+ const PASS_END = '\u0097'
104
+
105
+ // match passthrough slot
106
+ const PASS_SLOT_RX = new RegExp(`${PASS_START}(\\d+)${PASS_END}`, 'g')
107
+
108
+ // fix passthrough slot after syntax highlighting
109
+ const HIGHLIGHTED_PASS_SLOT_RX = new RegExp(
110
+ `<span\\b[^>]*>${PASS_START}</span>[^\\d]*(\\d+)[^\\d]*<span\\b[^>]*>${PASS_END}</span>`,
111
+ 'g'
112
+ )
113
+
114
+ const RS = '\\'
115
+ const R_SB = ']'
116
+ const ESC_R_SB = '\\]'
117
+ const PLUS = '+'
118
+
119
+ // ── Internal helpers ──────────────────────────────────────────────────────────
120
+
121
+ /**
122
+ * Ruby `str.partition(delim)` → `[before, delim, after]` (first occurrence).
123
+ * Returns `[str, '', '']` when delim is not found.
124
+ */
125
+ function partition(str, delim) {
126
+ const idx = str.indexOf(delim)
127
+ if (idx === -1) return [str, '', '']
128
+ return [str.slice(0, idx), delim, str.slice(idx + delim.length)]
129
+ }
130
+
131
+ /**
132
+ * Array union (Ruby `arr | other`).
133
+ */
134
+ function arrayUnion(a, b) {
135
+ const set = new Set(a)
136
+ for (const v of b) set.add(v)
137
+ return [...set]
138
+ }
139
+
140
+ /**
141
+ * Array intersection (Ruby `arr & other`): elements of a that appear in b, deduplicated,
142
+ * preserving the order from a with first occurrence winning.
143
+ */
144
+ function arrayIntersect(a, b) {
145
+ const allowed = new Set(b)
146
+ const seen = new Set()
147
+ return a.filter((v) => {
148
+ if (!allowed.has(v) || seen.has(v)) return false
149
+ seen.add(v)
150
+ return true
151
+ })
152
+ }
153
+
154
+ /**
155
+ * Array difference (Ruby `arr - other`).
156
+ */
157
+ function arrayDiff(a, b) {
158
+ const set = new Set(b)
159
+ return a.filter((v) => !set.has(v))
160
+ }
161
+
162
+ /**
163
+ * Make a regex global if it isn't already.
164
+ */
165
+ function globalRx(rx) {
166
+ return rx.global ? rx : new RegExp(rx.source, `${rx.flags}g`)
167
+ }
168
+
169
+ // ── Substitutors mixin ────────────────────────────────────────────────────────
170
+
171
+ export const Substitutors = {
172
+ // ── Public API ──────────────────────────────────────────────────────────────
173
+
174
+ /**
175
+ * Apply the specified substitutions to the text.
176
+ *
177
+ * @param {string|string[]} text - The text to process; must not be null.
178
+ * @param {string[]} [subs=NORMAL_SUBS] - The substitutions to perform.
179
+ * @returns {string|string[]} Text with substitutions applied.
180
+ */
181
+ async applySubs(text, subs = NORMAL_SUBS) {
182
+ const isEmpty = Array.isArray(text) ? text.length === 0 : text.length === 0
183
+ if (isEmpty || !subs || subs.length === 0) return text
184
+
185
+ const isMultiline = Array.isArray(text)
186
+ if (isMultiline) {
187
+ text = text.length > 1 ? text.join(LF) : text[0]
188
+ }
189
+
190
+ let passthrus
191
+ let clearPassthrus = false
192
+
193
+ if (subs.includes('macros')) {
194
+ text = this.extractPassthroughs(text)
195
+ if (this.passthroughs.length > 0) {
196
+ passthrus = this.passthroughs
197
+ // placeholders can move around; only clear in the outermost substitution call
198
+ if (!this.passthroughsLocked) {
199
+ this.passthroughsLocked = true
200
+ clearPassthrus = true
201
+ }
202
+ }
203
+ }
204
+
205
+ for (const type of subs) {
206
+ switch (type) {
207
+ case 'specialcharacters':
208
+ text = this.subSpecialchars(text)
209
+ break
210
+ case 'quotes':
211
+ text = await this.subQuotes(text)
212
+ break
213
+ case 'attributes':
214
+ if (text.includes(ATTR_REF_HEAD)) text = this.subAttributes(text)
215
+ break
216
+ case 'replacements':
217
+ text = this.subReplacements(text)
218
+ break
219
+ case 'macros':
220
+ text = await this.subMacros(text)
221
+ break
222
+ case 'highlight':
223
+ text = await this.highlightSource(text, subs.includes('callouts'))
224
+ break
225
+ case 'callouts':
226
+ if (!subs.includes('highlight')) text = await this.subCallouts(text)
227
+ break
228
+ case 'post_replacements':
229
+ text = await this.subPostReplacements(text)
230
+ break
231
+ default:
232
+ this.logger.warn(`unknown substitution type ${type}`)
233
+ }
234
+ }
235
+
236
+ if (passthrus) {
237
+ text = await this.restorePassthroughs(text)
238
+ if (clearPassthrus) {
239
+ passthrus.length = 0
240
+ this.passthroughsLocked = null
241
+ }
242
+ }
243
+
244
+ return isMultiline ? text.split(LF) : text
245
+ },
246
+
247
+ /** Apply normal substitutions (alias for applySubs with default args). */
248
+ async applyNormalSubs(text) {
249
+ return this.applySubs(text, NORMAL_SUBS)
250
+ },
251
+
252
+ /** Apply substitutions for header metadata and attribute assignments.
253
+ * Header subs are 'specialcharacters' + 'attributes', both of which are
254
+ * purely synchronous operations — so this method is intentionally sync
255
+ * to allow it to be called from synchronous contexts such as setAttribute()
256
+ * and the {set:...} directive inside subAttributes(). */
257
+ applyHeaderSubs(text) {
258
+ return this.subAttributes(this.subSpecialchars(text))
259
+ },
260
+
261
+ /** Apply substitutions for titles (alias for applySubs). */
262
+ async applyTitleSubs(text, subs = NORMAL_SUBS) {
263
+ return this.applySubs(text, subs)
264
+ },
265
+
266
+ /** Apply substitutions for reftext. */
267
+ async applyReftextSubs(text) {
268
+ return this.applySubs(text, REFTEXT_SUBS)
269
+ },
270
+
271
+ /**
272
+ * Substitute special characters (encode XML entities).
273
+ *
274
+ * @param {string} text
275
+ * @returns {string}
276
+ */
277
+ subSpecialchars(text) {
278
+ if (text.includes('>') || text.includes('&') || text.includes('<')) {
279
+ return text.replace(SPECIAL_CHARS_RX, (ch) => SPECIAL_CHARS_TR[ch])
280
+ }
281
+ return text
282
+ },
283
+
284
+ /** Alias for subSpecialchars. */
285
+ subSpecialcharacters(text) {
286
+ return this.subSpecialchars(text)
287
+ },
288
+
289
+ /**
290
+ * Substitute quoted text (emphasis, strong, monospaced, etc.)
291
+ *
292
+ * @param {string} text
293
+ * @returns {string}
294
+ */
295
+ async subQuotes(text) {
296
+ const compat = this.document.compatMode
297
+ if (QUOTED_TEXT_SNIFF_RX[compat].test(text)) {
298
+ for (const [type, scope, pattern] of QUOTE_SUBS[compat]) {
299
+ text = await asyncReplace(text, globalRx(pattern), async (...args) => {
300
+ return this.convertQuotedText(args, type, scope)
301
+ })
302
+ }
303
+ }
304
+ return text
305
+ },
306
+
307
+ /**
308
+ * Substitute attribute references in the specified text.
309
+ *
310
+ * @param {string} text
311
+ * @param {Object} [opts={}]
312
+ * @returns {string}
313
+ */
314
+ subAttributes(text, opts = {}) {
315
+ const docAttrs = this.document.attributes
316
+ let drop = false
317
+ let dropLine = false
318
+ let dropLineSeverity = null
319
+ let dropEmptyLine = false
320
+ let attributeUndefined = null
321
+ let attributeMissing = null
322
+
323
+ text = text.replace(
324
+ globalRx(AttributeReferenceRx),
325
+ (match, p1, p2, p3, p4) => {
326
+ // escaped attribute → return unescaped
327
+ if (p1 === RS || p4 === RS) {
328
+ return `{${p2}}`
329
+ }
330
+
331
+ if (p3) {
332
+ const args = p2.split(':', 3)
333
+ const directive = args.shift()
334
+ if (directive === 'set') {
335
+ const [, value] = Parser.storeAttribute(
336
+ args[0],
337
+ args[1] || '',
338
+ this.document
339
+ )
340
+ if (
341
+ (value !== null && value !== undefined) ||
342
+ (attributeUndefined ||=
343
+ docAttrs['attribute-undefined'] ||
344
+ Compliance.attributeUndefined) !== 'drop-line'
345
+ ) {
346
+ drop = true
347
+ dropEmptyLine = true
348
+ return DEL
349
+ } else {
350
+ drop = true
351
+ dropLine = true
352
+ return CAN
353
+ }
354
+ } else if (directive === 'counter2') {
355
+ this.document.counter(...args)
356
+ drop = true
357
+ dropEmptyLine = true
358
+ return DEL
359
+ } else {
360
+ // 'counter'
361
+ return this.document.counter(...args)
362
+ }
363
+ }
364
+
365
+ const key = p2.toLowerCase()
366
+ if (Object.hasOwn(docAttrs, key)) {
367
+ return docAttrs[key]
368
+ }
369
+
370
+ const intrinsicValue = INTRINSIC_ATTRIBUTES[key]
371
+ if (intrinsicValue !== undefined) return intrinsicValue
372
+
373
+ switch (
374
+ (attributeMissing ||=
375
+ opts.attributeMissing ||
376
+ docAttrs['attribute-missing'] ||
377
+ Compliance.attributeMissing)
378
+ ) {
379
+ case 'drop':
380
+ drop = true
381
+ dropEmptyLine = true
382
+ return DEL
383
+ case 'drop-line':
384
+ dropLineSeverity ||= opts.dropLineSeverity || 'info'
385
+ if (dropLineSeverity === 'info') {
386
+ this.logger.info(
387
+ `dropping line containing reference to missing attribute: ${key}`
388
+ )
389
+ }
390
+ drop = true
391
+ dropLine = true
392
+ return CAN
393
+ case 'warn':
394
+ this.logger.warn(`skipping reference to missing attribute: ${key}`)
395
+ return match
396
+ default: // 'skip'
397
+ return match
398
+ }
399
+ }
400
+ )
401
+
402
+ if (drop) {
403
+ if (dropEmptyLine) {
404
+ const lines = text.replace(new RegExp(`${DEL}+`, 'g'), DEL).split(LF)
405
+ if (dropLine) {
406
+ return lines
407
+ .filter(
408
+ (line) =>
409
+ line !== DEL &&
410
+ line !== CAN &&
411
+ !line.startsWith(CAN) &&
412
+ !line.includes(CAN)
413
+ )
414
+ .join(LF)
415
+ .split(DEL)
416
+ .join('')
417
+ } else {
418
+ return lines
419
+ .filter((line) => line !== DEL)
420
+ .join(LF)
421
+ .split(DEL)
422
+ .join('')
423
+ }
424
+ } else if (text.includes(LF)) {
425
+ return text
426
+ .split(LF)
427
+ .filter(
428
+ (line) =>
429
+ line !== CAN && !line.startsWith(CAN) && !line.includes(CAN)
430
+ )
431
+ .join(LF)
432
+ } else {
433
+ // When the caller sets returnDropSentinel, return null to signal that the line was
434
+ // dropped due to a *missing* attribute (as opposed to an attribute that simply has a
435
+ // blank value). This lets callers distinguish the two cases without changing the
436
+ // general contract of subAttributes for every other call-site.
437
+ return opts.returnDropSentinel ? null : ''
438
+ }
439
+ }
440
+
441
+ return text
442
+ },
443
+
444
+ /**
445
+ * Substitute replacement characters (copyright, trademark, etc.)
446
+ *
447
+ * @param {string} text
448
+ * @returns {string}
449
+ */
450
+ subReplacements(text) {
451
+ if (ReplaceableTextRx.test(text)) {
452
+ for (const [pattern, replacement, restore] of REPLACEMENTS) {
453
+ text = text.replace(globalRx(pattern), (...args) => {
454
+ return this.doReplacement(args, replacement, restore)
455
+ })
456
+ }
457
+ }
458
+ return text
459
+ },
460
+
461
+ /**
462
+ * Substitute inline macros (links, images, etc.)
463
+ *
464
+ * @param {string} text
465
+ * @returns {string}
466
+ */
467
+ async subMacros(text) {
468
+ const foundSquareBracket = text.includes('[')
469
+ const foundColon = text.includes(':')
470
+ const foundMacroish = foundSquareBracket && foundColon
471
+ const foundMacroishShort = foundMacroish && text.includes(':[')
472
+ const doc = this.document
473
+ const docAttrs = doc.attributes
474
+
475
+ // Extension inline macros
476
+ const extensions = doc.extensions
477
+ if (extensions?.inlineMacros()) {
478
+ for (const extension of extensions.inlineMacros()) {
479
+ text = await asyncReplace(
480
+ text,
481
+ globalRx(extension.instance.regexp),
482
+ async (...args) => {
483
+ const match = args[0]
484
+ if (match.startsWith(RS)) return match.slice(1)
485
+
486
+ const groups =
487
+ typeof args[args.length - 1] === 'object' &&
488
+ args[args.length - 1] !== null
489
+ ? args[args.length - 1]
490
+ : null
491
+ let target, content
492
+ if (!groups || Object.keys(groups).length === 0) {
493
+ target = args[1]
494
+ content = args[2]
495
+ } else {
496
+ target = groups.target ?? null
497
+ content = groups.content ?? null
498
+ }
499
+
500
+ const extConfig = extension.config
501
+ const defaultAttrs = extConfig.defaultAttrs
502
+ const attributes = defaultAttrs ? { ...defaultAttrs } : {}
503
+
504
+ if (content !== null && content !== undefined) {
505
+ if (!content) {
506
+ if (extConfig.contentModel !== 'attributes')
507
+ attributes.text = content
508
+ } else {
509
+ content = this.normalizeText(content, true, true)
510
+ if (extConfig.contentModel === 'attributes') {
511
+ await this.parseAttributes(
512
+ content,
513
+ extConfig.positionalAttrs || extConfig.posAttrs || [],
514
+ { into: attributes }
515
+ )
516
+ } else {
517
+ attributes.text = content
518
+ }
519
+ }
520
+ target = target ?? (extConfig.format === 'short' ? content : null)
521
+ }
522
+
523
+ const replacement = extension.processMethod(
524
+ this,
525
+ target,
526
+ attributes
527
+ )
528
+ if (replacement instanceof Inline) {
529
+ const inlineSubs = replacement.attributes.subs
530
+ if (inlineSubs) {
531
+ const expandedSubs = this.expandSubs(
532
+ inlineSubs,
533
+ 'custom inline macro'
534
+ )
535
+ if (expandedSubs)
536
+ replacement.text = await this.applySubs(
537
+ replacement.text,
538
+ expandedSubs
539
+ )
540
+ delete replacement.attributes.subs
541
+ }
542
+ return replacement.convert()
543
+ } else if (replacement) {
544
+ this.logger.info(
545
+ `expected substitution value for custom inline macro to be of type Inline; got ${replacement.constructor.name}: ${match}`
546
+ )
547
+ return replacement
548
+ }
549
+ return ''
550
+ }
551
+ )
552
+ }
553
+ }
554
+
555
+ // kbd / btn macros (experimental)
556
+ if (docAttrs.experimental !== undefined) {
557
+ if (
558
+ foundMacroishShort &&
559
+ (text.includes('kbd:') || text.includes('btn:'))
560
+ ) {
561
+ text = await asyncReplace(
562
+ text,
563
+ globalRx(InlineKbdBtnMacroRx),
564
+ async (match, p1, p2, p3) => {
565
+ if (p1) return match.slice(1)
566
+ if (p2 === 'kbd') {
567
+ let keys = p3.trim()
568
+ if (keys.includes(R_SB)) keys = keys.split(ESC_R_SB).join(R_SB)
569
+ if (keys.length > 1) {
570
+ let delimIdx = keys.indexOf(',', 1)
571
+ const plusIdx = keys.indexOf('+', 1)
572
+ if (delimIdx !== -1 && plusIdx !== -1)
573
+ delimIdx = Math.min(delimIdx, plusIdx)
574
+ else if (delimIdx === -1) delimIdx = plusIdx
575
+
576
+ if (delimIdx !== -1) {
577
+ const delim = keys.charAt(delimIdx)
578
+ if (keys.endsWith(delim)) {
579
+ keys = keys
580
+ .slice(0, -1)
581
+ .split(delim)
582
+ .map((k) => k.trim())
583
+ keys[keys.length - 1] += delim
584
+ } else {
585
+ keys = keys.split(delim).map((k) => k.trim())
586
+ }
587
+ } else {
588
+ keys = [keys]
589
+ }
590
+ } else {
591
+ keys = [keys]
592
+ }
593
+ return new Inline(this, 'kbd', null, {
594
+ attributes: { keys },
595
+ }).convert()
596
+ } else {
597
+ // btn
598
+ return new Inline(
599
+ this,
600
+ 'button',
601
+ this.normalizeText(p3, true, true)
602
+ ).convert()
603
+ }
604
+ }
605
+ )
606
+ }
607
+
608
+ if (foundMacroish && text.includes('menu:')) {
609
+ text = await asyncReplace(
610
+ text,
611
+ globalRx(InlineMenuMacroRx),
612
+ async (match, p1, p2) => {
613
+ if (match.startsWith(RS)) return match.slice(1)
614
+ const menu = p1
615
+ let submenus, menuitem
616
+ if (p2) {
617
+ const items = p2.includes(R_SB)
618
+ ? p2.split(ESC_R_SB).join(R_SB)
619
+ : p2
620
+ let delim = null
621
+ if (items.includes('&gt;')) delim = '&gt;'
622
+ else if (items.includes(',')) delim = ','
623
+ if (delim) {
624
+ const parts = items.split(delim).map((item) => item.trim())
625
+ menuitem = parts.pop()
626
+ submenus = parts
627
+ } else {
628
+ submenus = []
629
+ menuitem = items.trimEnd()
630
+ }
631
+ } else {
632
+ submenus = []
633
+ menuitem = null
634
+ }
635
+ return new Inline(this, 'menu', null, {
636
+ attributes: { menu, submenus, menuitem },
637
+ }).convert()
638
+ }
639
+ )
640
+ }
641
+
642
+ if (text.includes('"') && text.includes('&gt;')) {
643
+ text = await asyncReplace(
644
+ text,
645
+ globalRx(InlineMenuRx),
646
+ async (match, p1) => {
647
+ if (match.startsWith(RS)) return match.slice(1)
648
+ const parts = p1.split('&gt;').map((item) => item.trim())
649
+ const menu = parts.shift()
650
+ const menuitem = parts.pop() ?? null
651
+ const submenus = parts
652
+ return new Inline(this, 'menu', null, {
653
+ attributes: { menu, submenus, menuitem },
654
+ }).convert()
655
+ }
656
+ )
657
+ }
658
+ }
659
+
660
+ // image / icon macros
661
+ if (foundMacroish && (text.includes('image:') || text.includes('icon:'))) {
662
+ text = await asyncReplace(
663
+ text,
664
+ globalRx(InlineImageMacroRx),
665
+ async (match, p1, p2) => {
666
+ if (match.startsWith(RS)) return match.slice(1)
667
+ let type, posattrs
668
+ if (match.startsWith('icon:')) {
669
+ type = 'icon'
670
+ posattrs = ['size']
671
+ } else {
672
+ type = 'image'
673
+ posattrs = ['alt', 'width', 'height']
674
+ }
675
+ const target = p1
676
+ const attrs = await this.parseAttributes(p2, posattrs, {
677
+ unescapeInput: true,
678
+ })
679
+ let id
680
+ if (type !== 'icon') {
681
+ id = attrs.id
682
+ doc.register('images', target)
683
+ attrs.imagesdir = attrs.imagesdir ?? docAttrs.imagesdir
684
+ }
685
+ attrs.alt =
686
+ attrs.alt ??
687
+ (attrs['default-alt'] = basename(target, true).replace(
688
+ /[_-]/g,
689
+ ' '
690
+ ))
691
+ return new Inline(this, 'image', null, {
692
+ type,
693
+ target,
694
+ id,
695
+ attributes: attrs,
696
+ }).convert()
697
+ }
698
+ )
699
+ }
700
+
701
+ // index terms
702
+ if (
703
+ (text.includes('((') && text.includes('))')) ||
704
+ (foundMacroishShort && text.includes('dexterm'))
705
+ ) {
706
+ text = await asyncReplace(
707
+ text,
708
+ globalRx(InlineIndextermMacroRx),
709
+ async (match, p1, p2, p3) => {
710
+ switch (p1) {
711
+ case 'indexterm': {
712
+ if (match.startsWith(RS)) return match.slice(1)
713
+ const attrlist = this.normalizeText(p2, true, true)
714
+ let attrs
715
+ if (attrlist.includes('=')) {
716
+ const parsed = await new AttributeList(attrlist, this).parse()
717
+ const primary = parsed[1]
718
+ if (primary) {
719
+ const terms = [primary]
720
+ const secondary = parsed[2]
721
+ if (secondary) {
722
+ terms.push(secondary)
723
+ const tertiary = parsed[3]
724
+ if (tertiary) terms.push(tertiary)
725
+ }
726
+ attrs = { ...parsed, terms }
727
+ if (attrs['see-also']) {
728
+ const seeAlso = attrs['see-also']
729
+ attrs['see-also'] = seeAlso.includes(',')
730
+ ? seeAlso.split(',').map((s) => s.trimStart())
731
+ : [seeAlso]
732
+ }
733
+ } else {
734
+ attrs = { terms: attrlist }
735
+ }
736
+ } else {
737
+ attrs = { terms: this.splitSimpleCsv(attrlist) }
738
+ }
739
+ return new Inline(this, 'indexterm', null, {
740
+ attributes: attrs,
741
+ }).convert()
742
+ }
743
+ case 'indexterm2': {
744
+ if (match.startsWith(RS)) return match.slice(1)
745
+ let term = this.normalizeText(p2, true, true)
746
+ let attrs = null
747
+ if (term.includes('=')) {
748
+ const parsed = await new AttributeList(term, this).parse()
749
+ term = parsed[1] || term
750
+ if (parsed[1]) {
751
+ attrs = parsed
752
+ if (attrs['see-also']) {
753
+ attrs['see-also'] = attrs['see-also'].includes(',')
754
+ ? attrs['see-also'].split(',').map((s) => s.trimStart())
755
+ : [attrs['see-also']]
756
+ }
757
+ } else {
758
+ attrs = null
759
+ }
760
+ }
761
+ return new Inline(this, 'indexterm', term, {
762
+ attributes: attrs,
763
+ type: 'visible',
764
+ }).convert()
765
+ }
766
+ default: {
767
+ let enclText = p3
768
+ let visible = true,
769
+ before = null,
770
+ after = null
771
+ if (match.startsWith(RS)) {
772
+ if (enclText.startsWith('(') && enclText.endsWith(')')) {
773
+ enclText = enclText.slice(1, -1)
774
+ visible = true
775
+ before = '('
776
+ after = ')'
777
+ } else {
778
+ return match.slice(1)
779
+ }
780
+ } else {
781
+ if (enclText.startsWith('(')) {
782
+ if (enclText.endsWith(')')) {
783
+ enclText = enclText.slice(1, -1)
784
+ visible = false
785
+ } else {
786
+ enclText = enclText.slice(1)
787
+ before = '('
788
+ after = ''
789
+ }
790
+ } else if (enclText.endsWith(')')) {
791
+ enclText = enclText.slice(0, -1)
792
+ before = ''
793
+ after = ')'
794
+ }
795
+ }
796
+ let subbed_term
797
+ if (visible) {
798
+ let term = this.normalizeText(enclText, true)
799
+ let attrs = null
800
+ if (term.includes(';&')) {
801
+ if (term.includes(' &gt;&gt; ')) {
802
+ const [t, , see] = partition(term, ' &gt;&gt; ')
803
+ term = t
804
+ attrs = { see }
805
+ } else if (term.includes(' &amp;&gt; ')) {
806
+ const parts = term.split(' &amp;&gt; ')
807
+ term = parts.shift()
808
+ attrs = { 'see-also': parts }
809
+ }
810
+ }
811
+ subbed_term = await new Inline(this, 'indexterm', term, {
812
+ attributes: attrs,
813
+ type: 'visible',
814
+ }).convert()
815
+ } else {
816
+ const attrs = {}
817
+ let terms = this.normalizeText(enclText, true)
818
+ if (terms.includes(';&')) {
819
+ if (terms.includes(' &gt;&gt; ')) {
820
+ const [t, , see] = partition(terms, ' &gt;&gt; ')
821
+ terms = t
822
+ attrs.see = see
823
+ } else if (terms.includes(' &amp;&gt; ')) {
824
+ const parts = terms.split(' &amp;&gt; ')
825
+ terms = parts.shift()
826
+ attrs['see-also'] = parts
827
+ }
828
+ }
829
+ attrs.terms = this.splitSimpleCsv(terms)
830
+ subbed_term = await new Inline(this, 'indexterm', null, {
831
+ attributes: attrs,
832
+ }).convert()
833
+ }
834
+ return before !== null
835
+ ? `${before}${subbed_term}${after}`
836
+ : subbed_term
837
+ }
838
+ }
839
+ }
840
+ )
841
+ }
842
+
843
+ // inline URLs
844
+ if (foundColon && text.includes('://')) {
845
+ text = await asyncReplace(
846
+ text,
847
+ globalRx(InlineLinkRx),
848
+ async (match, p1, p2, p3, p4, p5, p6, p7, p8) => {
849
+ if (p2 && p5 == null) {
850
+ if (p1.startsWith(RS)) return match.slice(1)
851
+ if (p3.startsWith(RS)) return p1 + match.slice(p1.length + 1)
852
+ if (!p6) return match
853
+ const target = p3 + p6
854
+ doc.register('links', target)
855
+ const linkText =
856
+ docAttrs['hide-uri-scheme'] !== undefined
857
+ ? target.replace(UriSniffRx, '')
858
+ : target
859
+ return new Inline(this, 'anchor', linkText, {
860
+ type: 'link',
861
+ target,
862
+ attributes: { role: 'bare' },
863
+ }).convert()
864
+ } else {
865
+ if (p3.startsWith(RS)) return p1 + match.slice(p1.length + 1)
866
+ let prefix = p1
867
+ let target = p3 + (p4 || p7 || '')
868
+ let suffix = ''
869
+
870
+ let link_text
871
+ if (p5 != null) {
872
+ if (prefix === 'link:') prefix = ''
873
+ const rawLinkText = p5
874
+ link_text = rawLinkText || null
875
+ } else {
876
+ switch (prefix) {
877
+ case 'link:':
878
+ case '"':
879
+ case "'":
880
+ return match
881
+ }
882
+ switch (p8) {
883
+ case ';':
884
+ target = target.slice(0, -1)
885
+ if (target.endsWith(')')) {
886
+ target = target.slice(0, -1)
887
+ suffix = ');'
888
+ } else {
889
+ suffix = ';'
890
+ }
891
+ if (target === p3) return match
892
+ break
893
+ case ':':
894
+ target = target.slice(0, -1)
895
+ if (target.endsWith(')')) {
896
+ target = target.slice(0, -1)
897
+ suffix = '):'
898
+ } else {
899
+ suffix = ':'
900
+ }
901
+ if (target === p3) return match
902
+ break
903
+ }
904
+ link_text = null
905
+ }
906
+
907
+ const linkOpts = { type: 'link' }
908
+ let attrs = null
909
+ let bare = false
910
+
911
+ if (link_text !== null) {
912
+ let newLinkText = link_text.includes(R_SB)
913
+ ? link_text.split(ESC_R_SB).join(R_SB)
914
+ : link_text
915
+ link_text = newLinkText
916
+
917
+ if (!doc.compatMode && link_text.includes('=')) {
918
+ const [extractedText, extractedAttrs] =
919
+ await this.extractAttributesFromText(link_text, '')
920
+ link_text = extractedText
921
+ newLinkText = extractedText
922
+ attrs = extractedAttrs
923
+ linkOpts.id = attrs?.id
924
+ }
925
+
926
+ if (link_text.endsWith('^')) {
927
+ newLinkText = link_text = link_text.slice(0, -1)
928
+ if (attrs) {
929
+ attrs.window = attrs.window ?? '_blank'
930
+ } else {
931
+ attrs = { window: '_blank' }
932
+ }
933
+ }
934
+
935
+ if (
936
+ newLinkText !== null &&
937
+ newLinkText !== undefined &&
938
+ newLinkText === ''
939
+ ) {
940
+ link_text =
941
+ docAttrs['hide-uri-scheme'] !== undefined
942
+ ? target.replace(UriSniffRx, '')
943
+ : target
944
+ bare = true
945
+ }
946
+ } else {
947
+ link_text =
948
+ docAttrs['hide-uri-scheme'] !== undefined
949
+ ? target.replace(UriSniffRx, '')
950
+ : target
951
+ bare = true
952
+ }
953
+
954
+ if (bare) {
955
+ if (attrs) {
956
+ attrs.role =
957
+ 'role' in (attrs || {}) ? `bare ${attrs.role}` : 'bare'
958
+ } else {
959
+ attrs = { role: 'bare' }
960
+ }
961
+ }
962
+
963
+ linkOpts.target = target
964
+ doc.register('links', target)
965
+ if (attrs) linkOpts.attributes = attrs
966
+ return `${prefix}${await new Inline(this, 'anchor', link_text, linkOpts).convert()}${suffix}`
967
+ }
968
+ }
969
+ )
970
+ }
971
+
972
+ // link: and mailto: macros
973
+ if (foundMacroish && (text.includes('link:') || text.includes('ilto:'))) {
974
+ text = await asyncReplace(
975
+ text,
976
+ globalRx(InlineLinkMacroRx),
977
+ async (match, p1, p2, p3) => {
978
+ if (match.startsWith(RS)) return match.slice(1)
979
+ let target, mailtoText
980
+ if (p1) {
981
+ mailtoText = p2
982
+ target = `mailto:${mailtoText}`
983
+ } else {
984
+ target = p2
985
+ }
986
+
987
+ let attrs = null
988
+ const linkOpts = { type: 'link' }
989
+ let linkText = p3
990
+
991
+ if (linkText) {
992
+ linkText = linkText.includes(R_SB)
993
+ ? linkText.split(ESC_R_SB).join(R_SB)
994
+ : linkText
995
+ if (p1) {
996
+ if (!doc.compatMode && linkText.includes(',')) {
997
+ const [extractedText, extractedAttrs] =
998
+ await this.extractAttributesFromText(linkText, '')
999
+ linkText = extractedText
1000
+ attrs = extractedAttrs
1001
+ linkOpts.id = attrs?.id
1002
+ if (attrs?.[2]) {
1003
+ if (attrs[3]) {
1004
+ target = `${target}?subject=${encodeUriComponent(attrs[2])}&amp;body=${encodeUriComponent(attrs[3])}`
1005
+ } else {
1006
+ target = `${target}?subject=${encodeUriComponent(attrs[2])}`
1007
+ }
1008
+ }
1009
+ }
1010
+ } else if (!doc.compatMode && linkText.includes('=')) {
1011
+ const [extractedText, extractedAttrs] =
1012
+ await this.extractAttributesFromText(linkText, '')
1013
+ linkText = extractedText
1014
+ attrs = extractedAttrs
1015
+ linkOpts.id = attrs?.id
1016
+ }
1017
+
1018
+ if (linkText.endsWith('^')) {
1019
+ linkText = linkText.slice(0, -1)
1020
+ if (attrs) {
1021
+ attrs.window = attrs.window ?? '_blank'
1022
+ } else {
1023
+ attrs = { window: '_blank' }
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ if (!linkText) {
1029
+ if (p1) {
1030
+ linkText = mailtoText
1031
+ } else {
1032
+ if (docAttrs['hide-uri-scheme'] !== undefined) {
1033
+ linkText = target.replace(UriSniffRx, '') || target
1034
+ } else {
1035
+ linkText = target
1036
+ }
1037
+ if (attrs) {
1038
+ attrs.role = 'role' in attrs ? `bare ${attrs.role}` : 'bare'
1039
+ } else {
1040
+ attrs = { role: 'bare' }
1041
+ }
1042
+ }
1043
+ }
1044
+
1045
+ linkOpts.target = target
1046
+ doc.register('links', target)
1047
+ if (attrs) linkOpts.attributes = attrs
1048
+ return new Inline(this, 'anchor', linkText, linkOpts).convert()
1049
+ }
1050
+ )
1051
+ }
1052
+
1053
+ // bare email addresses
1054
+ if (text.includes('@')) {
1055
+ text = await asyncReplace(
1056
+ text,
1057
+ globalRx(InlineEmailRx),
1058
+ async (match, p1) => {
1059
+ if (p1) return p1 === RS ? match.slice(1) : match
1060
+ const address = match
1061
+ const target = `mailto:${address}`
1062
+ doc.register('links', target)
1063
+ return new Inline(this, 'anchor', address, {
1064
+ type: 'link',
1065
+ target,
1066
+ }).convert()
1067
+ }
1068
+ )
1069
+ }
1070
+
1071
+ // bibliography anchor
1072
+ if (
1073
+ foundSquareBracket &&
1074
+ this.context === 'list_item' &&
1075
+ this.getParent().style === 'bibliography'
1076
+ ) {
1077
+ text = await asyncReplace(
1078
+ text,
1079
+ InlineBiblioAnchorRx,
1080
+ async (match, p1, p2) => {
1081
+ return new Inline(this, 'anchor', p2, {
1082
+ type: 'bibref',
1083
+ id: p1,
1084
+ }).convert()
1085
+ }
1086
+ )
1087
+ }
1088
+
1089
+ // inline anchors
1090
+ if (
1091
+ (foundSquareBracket && text.includes('[[')) ||
1092
+ (foundMacroish && text.includes('or:'))
1093
+ ) {
1094
+ text = await asyncReplace(
1095
+ text,
1096
+ globalRx(InlineAnchorRx),
1097
+ async (match, p1, p2, p3, p4, p5) => {
1098
+ if (p1) return match.slice(1)
1099
+ let id, reftext
1100
+ if (p2) {
1101
+ id = p2
1102
+ reftext = p3
1103
+ } else {
1104
+ id = p4
1105
+ reftext = p5
1106
+ ? p5.includes(R_SB)
1107
+ ? p5.split(ESC_R_SB).join(R_SB)
1108
+ : p5
1109
+ : null
1110
+ }
1111
+ return new Inline(this, 'anchor', reftext, {
1112
+ type: 'ref',
1113
+ id,
1114
+ }).convert()
1115
+ }
1116
+ )
1117
+ }
1118
+
1119
+ // xref macros
1120
+ if (
1121
+ (text.includes('&') && text.includes(';&l')) ||
1122
+ (foundMacroish && text.includes('xref:'))
1123
+ ) {
1124
+ text = await asyncReplace(
1125
+ text,
1126
+ globalRx(InlineXrefMacroRx),
1127
+ async (match, p1, p2, p3) => {
1128
+ if (match.startsWith(RS)) return match.slice(1)
1129
+ const attrs = {}
1130
+ let refid, linkText, macro, path, fragment, target, src2src
1131
+
1132
+ if (p1) {
1133
+ refid = p1
1134
+ if (refid.includes(',')) {
1135
+ const commaIdx = refid.indexOf(',')
1136
+ const rawLinkText = refid.slice(commaIdx + 1).trimStart()
1137
+ refid = refid.slice(0, commaIdx)
1138
+ linkText = rawLinkText || null
1139
+ }
1140
+ } else {
1141
+ macro = true
1142
+ refid = p2
1143
+ if (p3) {
1144
+ linkText = p3.includes(R_SB) ? p3.split(ESC_R_SB).join(R_SB) : p3
1145
+ if (!doc.compatMode && linkText.includes('=')) {
1146
+ const [extractedText, extractedAttrs] =
1147
+ await this.extractAttributesFromText(linkText)
1148
+ linkText = extractedText
1149
+ Object.assign(attrs, extractedAttrs)
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ if (doc.compatMode) {
1155
+ fragment = refid
1156
+ } else {
1157
+ const hashIdx = refid.indexOf('#')
1158
+ if (
1159
+ hashIdx !== -1 &&
1160
+ (hashIdx === 0 || refid[hashIdx - 1] !== '&')
1161
+ ) {
1162
+ if (hashIdx > 0) {
1163
+ const fragmentLen = refid.length - 1 - hashIdx
1164
+ if (fragmentLen > 0) {
1165
+ path = refid.slice(0, hashIdx)
1166
+ fragment = refid.slice(hashIdx + 1)
1167
+ } else {
1168
+ path = refid.slice(0, -1)
1169
+ }
1170
+ if (macro) {
1171
+ if (path.endsWith('.adoc')) {
1172
+ src2src = path = path.slice(0, -5)
1173
+ } else if (!isExtname(path)) {
1174
+ src2src = path
1175
+ }
1176
+ } else if (
1177
+ Object.keys(ASCIIDOC_EXTENSIONS).some((ext) =>
1178
+ path.endsWith(ext)
1179
+ )
1180
+ ) {
1181
+ src2src = path = path.slice(0, path.lastIndexOf('.'))
1182
+ } else {
1183
+ src2src = path
1184
+ }
1185
+ } else {
1186
+ target = refid
1187
+ fragment = refid.slice(1)
1188
+ }
1189
+ } else if (macro) {
1190
+ if (refid.endsWith('.adoc')) {
1191
+ src2src = path = refid.slice(0, -5)
1192
+ } else if (isExtname(refid)) {
1193
+ path = refid
1194
+ } else {
1195
+ fragment = refid
1196
+ }
1197
+ } else {
1198
+ fragment = refid
1199
+ }
1200
+ }
1201
+
1202
+ if (target) {
1203
+ // handles: #id
1204
+ refid = fragment
1205
+ if (this.logger.isInfo?.() && !doc.catalog.refs[refid]) {
1206
+ this.logger.info(`possible invalid reference: ${refid}`)
1207
+ }
1208
+ } else if (path) {
1209
+ if (
1210
+ src2src &&
1211
+ (doc.attributes.docname === path || doc.catalog.includes[path])
1212
+ ) {
1213
+ if (fragment) {
1214
+ refid = fragment
1215
+ path = null
1216
+ target = `#${fragment}`
1217
+ if (this.logger.isInfo?.() && !doc.catalog.refs[refid]) {
1218
+ this.logger.info(`possible invalid reference: ${refid}`)
1219
+ }
1220
+ } else {
1221
+ refid = null
1222
+ path = null
1223
+ target = '#'
1224
+ }
1225
+ } else {
1226
+ const relfileprefix = doc.attributes.relfileprefix || ''
1227
+ const relfilesuffix = src2src
1228
+ ? (doc.attributes.relfilesuffix ?? doc.outfilesuffix)
1229
+ : ''
1230
+ const resolvedPath = `${relfileprefix}${path}${relfilesuffix}`
1231
+ refid = path
1232
+ path = resolvedPath
1233
+ if (fragment) {
1234
+ refid = `${refid}#${fragment}`
1235
+ target = `${path}#${fragment}`
1236
+ } else {
1237
+ target = path
1238
+ }
1239
+ }
1240
+ } else if (doc.compatMode || !Compliance.naturalXrefs) {
1241
+ refid = fragment
1242
+ target = `#${fragment}`
1243
+ if (this.logger.isInfo?.() && !doc.catalog.refs[refid]) {
1244
+ this.logger.info(`possible invalid reference: ${refid}`)
1245
+ }
1246
+ } else if (doc.catalog.refs[fragment]) {
1247
+ refid = fragment
1248
+ target = `#${fragment}`
1249
+ } else if (
1250
+ (fragment.includes(' ') || fragment.toLowerCase() !== fragment) &&
1251
+ (refid = await doc.resolveId(fragment))
1252
+ ) {
1253
+ fragment = refid
1254
+ target = `#${refid}`
1255
+ } else {
1256
+ refid = fragment
1257
+ target = `#${fragment}`
1258
+ if (this.logger.isInfo?.())
1259
+ this.logger.info(`possible invalid reference: ${refid}`)
1260
+ }
1261
+
1262
+ if (path != null) attrs.path = path
1263
+ if (fragment != null) attrs.fragment = fragment
1264
+ attrs.refid = refid
1265
+ return new Inline(this, 'anchor', linkText, {
1266
+ type: 'xref',
1267
+ target,
1268
+ attributes: attrs,
1269
+ }).convert()
1270
+ }
1271
+ )
1272
+ }
1273
+
1274
+ // footnote macros
1275
+ if (foundMacroish && text.includes('tnote')) {
1276
+ text = await asyncReplace(
1277
+ text,
1278
+ globalRx(InlineFootnoteMacroRx),
1279
+ async (match, p1, p2, p3) => {
1280
+ if (match.startsWith(RS)) return match.slice(1)
1281
+
1282
+ let id, content, type, target
1283
+ if (p1) {
1284
+ // footnoteref
1285
+ if (p3) {
1286
+ const commaIdx = p3.indexOf(',')
1287
+ if (commaIdx >= 0) {
1288
+ id = p3.slice(0, commaIdx)
1289
+ content = p3.slice(commaIdx + 1)
1290
+ } else {
1291
+ // reference only (no text), e.g. footnoteref:[id]
1292
+ id = p3
1293
+ }
1294
+ if (!doc.compatMode) {
1295
+ this.logger.warn(
1296
+ `found deprecated footnoteref macro: ${match}; use footnote macro with target instead`
1297
+ )
1298
+ }
1299
+ } else {
1300
+ return match
1301
+ }
1302
+ } else {
1303
+ id = p2
1304
+ content = p3
1305
+ }
1306
+
1307
+ let index
1308
+ if (id) {
1309
+ const footnote = doc.footnotes.find((f) => f.id === id)
1310
+ if (footnote) {
1311
+ index = footnote.index
1312
+ content = footnote.text
1313
+ type = 'xref'
1314
+ target = id
1315
+ id = null
1316
+ } else if (content) {
1317
+ content = await this.restorePassthroughs(
1318
+ this.normalizeText(content, true, true)
1319
+ )
1320
+ index = doc.counter('footnote-number')
1321
+ doc.register('footnotes', new Footnote(index, id, content))
1322
+ type = 'ref'
1323
+ target = null
1324
+ } else {
1325
+ this.logger.warn(`invalid footnote reference: ${id}`)
1326
+ type = 'xref'
1327
+ target = id
1328
+ content = id
1329
+ id = null
1330
+ }
1331
+ } else if (content) {
1332
+ content = await this.restorePassthroughs(
1333
+ this.normalizeText(content, true, true)
1334
+ )
1335
+ index = doc.counter('footnote-number')
1336
+ doc.register('footnotes', new Footnote(index, id, content))
1337
+ type = null
1338
+ target = null
1339
+ } else {
1340
+ return match
1341
+ }
1342
+
1343
+ return new Inline(this, 'footnote', content, {
1344
+ attributes: { index },
1345
+ id,
1346
+ target,
1347
+ type,
1348
+ }).convert()
1349
+ }
1350
+ )
1351
+ }
1352
+
1353
+ return text
1354
+ },
1355
+
1356
+ /**
1357
+ * Substitute post replacements (hard line breaks).
1358
+ *
1359
+ * @param {string} text
1360
+ * @returns {string}
1361
+ */
1362
+ async subPostReplacements(text) {
1363
+ if (
1364
+ 'hardbreaks-option' in this.attributes ||
1365
+ 'hardbreaks-option' in this.document.attributes
1366
+ ) {
1367
+ const lines = text.split(LF)
1368
+ if (lines.length < 2) return text
1369
+ const last = lines.pop()
1370
+ const converted = await Promise.all(
1371
+ lines.map((line) =>
1372
+ new Inline(
1373
+ this,
1374
+ 'break',
1375
+ line.endsWith(HARD_LINE_BREAK) ? line.slice(0, -2) : line,
1376
+ { type: 'line' }
1377
+ ).convert()
1378
+ )
1379
+ )
1380
+ return [...converted, last].join(LF)
1381
+ } else if (text.includes(PLUS) && text.includes(HARD_LINE_BREAK)) {
1382
+ return asyncReplace(
1383
+ text,
1384
+ globalRx(HardLineBreakRx),
1385
+ async (match, p1) => {
1386
+ return new Inline(this, 'break', p1, { type: 'line' }).convert()
1387
+ }
1388
+ )
1389
+ }
1390
+ return text
1391
+ },
1392
+
1393
+ /**
1394
+ * Apply verbatim substitutions on source.
1395
+ *
1396
+ * @param {string} source
1397
+ * @param {boolean} processCallouts
1398
+ * @returns {string}
1399
+ */
1400
+ async subSource(source, processCallouts) {
1401
+ return processCallouts
1402
+ ? await this.subCallouts(this.subSpecialchars(source))
1403
+ : this.subSpecialchars(source)
1404
+ },
1405
+
1406
+ /**
1407
+ * Substitute callout source references.
1408
+ *
1409
+ * @param {string} text
1410
+ * @returns {string}
1411
+ */
1412
+ async subCallouts(text) {
1413
+ const calloutRx = this.hasAttribute('line-comment')
1414
+ ? CalloutSourceRxMap[this.getAttribute('line-comment')]
1415
+ : CalloutSourceRx
1416
+ let autonum = 0
1417
+ return asyncReplace(
1418
+ text,
1419
+ globalRx(calloutRx),
1420
+ async (match, p1, p2, p3, p4) => {
1421
+ if (p2) {
1422
+ return match.replace(RS, '')
1423
+ }
1424
+ const guard = p1 || (p3 === '--' ? ['<!--', '-->'] : null)
1425
+ const numeral = p4 === '.' ? String(++autonum) : p4
1426
+ return new Inline(this, 'callout', numeral, {
1427
+ id: this.document.callouts.readNextId(),
1428
+ attributes: { guard },
1429
+ }).convert()
1430
+ }
1431
+ )
1432
+ },
1433
+
1434
+ /**
1435
+ * Highlight (colorize) the source code using a syntax highlighter.
1436
+ *
1437
+ * @param {string} source
1438
+ * @param {boolean} processCallouts
1439
+ * @returns {string}
1440
+ */
1441
+ async highlightSource(source, processCallouts) {
1442
+ const syntaxHl = this.document.syntaxHighlighter
1443
+ if (!syntaxHl?.handlesHighlighting()) {
1444
+ return this.subSource(source, processCallouts)
1445
+ }
1446
+
1447
+ let calloutMarks
1448
+ if (processCallouts) {
1449
+ ;[source, calloutMarks] = this.extractCallouts(source)
1450
+ }
1451
+
1452
+ const docAttrs = this.document.attributes
1453
+ const syntaxHlName = syntaxHl.name
1454
+ let linenumsMode = null
1455
+ let startLineNumber = null
1456
+ if (this.hasOption('linenums')) {
1457
+ linenumsMode = docAttrs[`${syntaxHlName}-linenums-mode`] || 'table'
1458
+ startLineNumber = parseInt(this.getAttribute('start', 1), 10)
1459
+ if (startLineNumber < 1) startLineNumber = 1
1460
+ }
1461
+
1462
+ let highlightLines = null
1463
+ if (this.hasAttribute('highlight')) {
1464
+ highlightLines = this.resolveLinesToHighlight(
1465
+ source,
1466
+ this.getAttribute('highlight'),
1467
+ startLineNumber
1468
+ )
1469
+ }
1470
+
1471
+ const hlResult = syntaxHl.highlight(
1472
+ this,
1473
+ source,
1474
+ this.getAttribute('language'),
1475
+ {
1476
+ callouts: calloutMarks,
1477
+ cssMode: docAttrs[`${syntaxHlName}-css`] || 'class',
1478
+ highlightLines,
1479
+ numberLines: linenumsMode,
1480
+ startLineNumber,
1481
+ style: docAttrs[`${syntaxHlName}-style`],
1482
+ }
1483
+ )
1484
+ const [highlighted, sourceOffset] = Array.isArray(hlResult)
1485
+ ? hlResult
1486
+ : [hlResult, undefined]
1487
+
1488
+ let result = highlighted
1489
+ if (this.passthroughs.length > 0) {
1490
+ result = result.replace(
1491
+ globalRx(HIGHLIGHTED_PASS_SLOT_RX),
1492
+ `${PASS_START}$1${PASS_END}`
1493
+ )
1494
+ }
1495
+
1496
+ if (!calloutMarks || Object.keys(calloutMarks).length === 0) {
1497
+ return result
1498
+ }
1499
+ return await this.restoreCallouts(result, calloutMarks, sourceOffset)
1500
+ },
1501
+
1502
+ /**
1503
+ * Resolve line numbers to highlight from a test string.
1504
+ *
1505
+ * @param {string} source
1506
+ * @param {string} spec - e.g. "1-5, !2, 10" or "1..5;!2;10"
1507
+ * @param {number|null} [start=null]
1508
+ * @returns {number[]}
1509
+ */
1510
+ resolveLinesToHighlight(source, spec, start = null) {
1511
+ let lines = []
1512
+ if (spec.includes(' ')) spec = spec.split(' ').join('')
1513
+ const entries = spec.includes(',') ? spec.split(',') : spec.split(';')
1514
+
1515
+ for (let entry of entries) {
1516
+ let negate = false
1517
+ if (entry.startsWith('!')) {
1518
+ entry = entry.slice(1)
1519
+ negate = true
1520
+ }
1521
+ const delim = entry.includes('..')
1522
+ ? '..'
1523
+ : entry.includes('-')
1524
+ ? '-'
1525
+ : null
1526
+ if (delim) {
1527
+ const [fromStr, , toStr] = partition(entry, delim)
1528
+ const from = parseInt(fromStr, 10)
1529
+ let to
1530
+ if (!toStr || (to = parseInt(toStr, 10)) < 0) {
1531
+ to = source.split(LF).length + 1
1532
+ }
1533
+ const range = Array.from({ length: to - from + 1 }, (_, i) => from + i)
1534
+ if (negate) {
1535
+ lines = arrayDiff(lines, range)
1536
+ } else {
1537
+ lines = arrayUnion(lines, range)
1538
+ }
1539
+ } else if (negate) {
1540
+ const val = parseInt(entry, 10)
1541
+ lines = lines.filter((l) => l !== val)
1542
+ } else {
1543
+ const line = parseInt(entry, 10)
1544
+ if (!lines.includes(line)) lines.push(line)
1545
+ }
1546
+ }
1547
+
1548
+ if (start) {
1549
+ const shift = start - 1
1550
+ if (shift !== 0) lines = lines.map((l) => l - shift)
1551
+ }
1552
+
1553
+ return lines.sort((a, b) => a - b)
1554
+ },
1555
+
1556
+ /**
1557
+ * Extract passthrough text for reinsertion after processing.
1558
+ *
1559
+ * @param {string} text
1560
+ * @returns {string} Text with passthrough regions replaced by placeholders.
1561
+ */
1562
+ extractPassthroughs(text) {
1563
+ const compatMode = this.document.compatMode
1564
+ const passthrus = this.passthroughs
1565
+
1566
+ if (text.includes('++') || text.includes('$$') || text.includes('ss:')) {
1567
+ text = text.replace(
1568
+ globalRx(InlinePassMacroRx),
1569
+ (match, p1, p2, p3, p4, p5, p6, p7, p8) => {
1570
+ const boundary = p4 // $$, ++, or +++
1571
+ if (boundary) {
1572
+ // skip ++ in compat mode
1573
+ if (compatMode && boundary === '++') {
1574
+ const prefix = p2 ? `${p1}[${p2}]${p3}` : `${p1}${p3}`
1575
+ return `${prefix}++${this.extractPassthroughs(p5)}++`
1576
+ }
1577
+
1578
+ let attributes, oldBehavior, preceding
1579
+ if (p2) {
1580
+ const attrlist = p2
1581
+ const escapeCount = p3.length
1582
+ if (escapeCount > 0) {
1583
+ return `${p1}[${attrlist}]${RS.repeat(escapeCount - 1)}${boundary}${p5}${boundary}`
1584
+ } else if (p1 === RS) {
1585
+ preceding = `[${attrlist}]`
1586
+ } else if (boundary === '++') {
1587
+ if (attrlist === 'x-') {
1588
+ oldBehavior = true
1589
+ attributes = {}
1590
+ } else if (attrlist.endsWith(' x-')) {
1591
+ oldBehavior = true
1592
+ attributes = this.parseQuotedTextAttributes(
1593
+ attrlist.slice(0, -3)
1594
+ )
1595
+ } else {
1596
+ attributes = this.parseQuotedTextAttributes(attrlist)
1597
+ }
1598
+ } else {
1599
+ attributes = this.parseQuotedTextAttributes(attrlist)
1600
+ }
1601
+ } else {
1602
+ const escapeCount = p3.length
1603
+ if (escapeCount > 0) {
1604
+ return `${RS.repeat(escapeCount - 1)}${boundary}${p5}${boundary}`
1605
+ }
1606
+ }
1607
+
1608
+ const subs = boundary === '+++' ? [] : [...BASIC_SUBS]
1609
+ let passthruKey
1610
+ if (attributes) {
1611
+ if (oldBehavior) {
1612
+ passthrus[(passthruKey = passthrus.length)] = {
1613
+ text: p5,
1614
+ subs: NORMAL_SUBS,
1615
+ type: 'monospaced',
1616
+ attributes,
1617
+ }
1618
+ } else {
1619
+ passthrus[(passthruKey = passthrus.length)] = {
1620
+ text: p5,
1621
+ subs,
1622
+ type: 'unquoted',
1623
+ attributes,
1624
+ }
1625
+ }
1626
+ } else {
1627
+ passthrus[(passthruKey = passthrus.length)] = { text: p5, subs }
1628
+ }
1629
+ return `${preceding || ''}${PASS_START}${passthruKey}${PASS_END}`
1630
+ } else {
1631
+ // pass:[]
1632
+ if (p6 === RS) return match.slice(1)
1633
+ let passthruKey
1634
+ if (p7) {
1635
+ passthrus[(passthruKey = passthrus.length)] = {
1636
+ text: this.normalizeText(p8, null, true),
1637
+ subs: this.resolvePassSubs(p7),
1638
+ }
1639
+ } else {
1640
+ passthrus[(passthruKey = passthrus.length)] = {
1641
+ text: this.normalizeText(p8, null, true),
1642
+ }
1643
+ }
1644
+ return `${PASS_START}${passthruKey}${PASS_END}`
1645
+ }
1646
+ }
1647
+ )
1648
+ }
1649
+
1650
+ const [passInlineChar1, passInlineChar2, passInlineRx] =
1651
+ InlinePassRx[compatMode]
1652
+
1653
+ if (
1654
+ text.includes(passInlineChar1) ||
1655
+ (passInlineChar2 && text.includes(passInlineChar2))
1656
+ ) {
1657
+ text = text.replace(
1658
+ globalRx(passInlineRx),
1659
+ (match, p1, p2, p3, p4, p5, p6, p7, p8) => {
1660
+ const preceding = p1
1661
+ const attrlist = p4 || p3
1662
+ const escaped = !!p5
1663
+ const quotedText = p6
1664
+ const formatMark = p7
1665
+ const content = p8
1666
+
1667
+ let oldBehavior, oldBehaviorForced, attributes
1668
+
1669
+ if (compatMode) {
1670
+ oldBehavior = true
1671
+ } else if (
1672
+ attrlist &&
1673
+ (attrlist === 'x-' || attrlist.endsWith(' x-'))
1674
+ ) {
1675
+ oldBehavior = true
1676
+ oldBehaviorForced = true
1677
+ }
1678
+
1679
+ if (attrlist) {
1680
+ if (escaped) {
1681
+ return `${preceding}[${attrlist}]${quotedText.slice(1)}`
1682
+ } else if (preceding === RS) {
1683
+ if (oldBehaviorForced && formatMark === '`') {
1684
+ return `${preceding}[${attrlist}]${quotedText}`
1685
+ }
1686
+ if (compatMode && formatMark === '`') {
1687
+ // escaped role in compat-mode: role becomes literal text, backtick span still processed as monospaced
1688
+ let passthruKey
1689
+ passthrus[(passthruKey = passthrus.length)] = {
1690
+ text: content,
1691
+ subs: BASIC_SUBS,
1692
+ type: 'monospaced',
1693
+ }
1694
+ return `[${attrlist}]${PASS_START}${passthruKey}${PASS_END}`
1695
+ }
1696
+ return `[${attrlist}]${quotedText}` // preceding replaced by attrlist form
1697
+ } else if (oldBehaviorForced) {
1698
+ attributes =
1699
+ attrlist === 'x-'
1700
+ ? {}
1701
+ : this.parseQuotedTextAttributes(attrlist.slice(0, -3))
1702
+ } else {
1703
+ attributes = this.parseQuotedTextAttributes(attrlist)
1704
+ }
1705
+ } else if (escaped) {
1706
+ return `${preceding}${quotedText.slice(1)}`
1707
+ } else if (compatMode && preceding === RS) {
1708
+ return quotedText
1709
+ }
1710
+
1711
+ let passthruKey
1712
+ if (compatMode) {
1713
+ passthrus[(passthruKey = passthrus.length)] = {
1714
+ text: content,
1715
+ subs: BASIC_SUBS,
1716
+ attributes,
1717
+ type: 'monospaced',
1718
+ }
1719
+ } else if (attributes) {
1720
+ if (oldBehavior) {
1721
+ const subs = formatMark === '`' ? BASIC_SUBS : NORMAL_SUBS
1722
+ passthrus[(passthruKey = passthrus.length)] = {
1723
+ text: content,
1724
+ subs,
1725
+ attributes,
1726
+ type: 'monospaced',
1727
+ }
1728
+ } else {
1729
+ passthrus[(passthruKey = passthrus.length)] = {
1730
+ text: content,
1731
+ subs: BASIC_SUBS,
1732
+ attributes,
1733
+ type: 'unquoted',
1734
+ }
1735
+ }
1736
+ } else {
1737
+ passthrus[(passthruKey = passthrus.length)] = {
1738
+ text: content,
1739
+ subs: BASIC_SUBS,
1740
+ }
1741
+ }
1742
+
1743
+ return `${preceding || ''}${PASS_START}${passthruKey}${PASS_END}`
1744
+ }
1745
+ )
1746
+ }
1747
+
1748
+ // stem macros (in a subsequent step to allow escaping by the former)
1749
+ if (
1750
+ text.includes(':') &&
1751
+ (text.includes('stem:') || text.includes('math:'))
1752
+ ) {
1753
+ text = text.replace(globalRx(InlineStemMacroRx), (match, p1, p2, p3) => {
1754
+ if (match.startsWith(RS)) return match.slice(1)
1755
+ let type = p1
1756
+ if (type === 'stem') {
1757
+ type = STEM_TYPE_ALIASES[this.document.attributes.stem]
1758
+ }
1759
+ let content = this.normalizeText(p3, null, true)
1760
+ if (
1761
+ type === 'latexmath' &&
1762
+ content.startsWith('$') &&
1763
+ content.endsWith('$')
1764
+ ) {
1765
+ content = content.slice(1, -1)
1766
+ }
1767
+ const subs = p2
1768
+ ? this.resolvePassSubs(p2, 'stem macro')
1769
+ : this.document.basebackend('html')
1770
+ ? BASIC_SUBS
1771
+ : null
1772
+ const passthruKey = passthrus.length
1773
+ passthrus[passthruKey] = { text: content, subs, type }
1774
+ return `${PASS_START}${passthruKey}${PASS_END}`
1775
+ })
1776
+ }
1777
+
1778
+ return text
1779
+ },
1780
+
1781
+ /**
1782
+ * Restore passthrough text by reinserting into placeholder positions.
1783
+ *
1784
+ * @param {string} text
1785
+ * @returns {string}
1786
+ */
1787
+ async restorePassthroughs(text) {
1788
+ if (!text.includes(PASS_START)) return text
1789
+ const passthrus = this.passthroughs
1790
+ return asyncReplace(text, globalRx(PASS_SLOT_RX), async (match, p1) => {
1791
+ const pass = passthrus[parseInt(p1, 10)]
1792
+ if (pass) {
1793
+ let subbedText = await this.applySubs(pass.text, pass.subs)
1794
+ const type = pass.type
1795
+ if (type) {
1796
+ const attributes = pass.attributes
1797
+ const id = attributes?.id
1798
+ subbedText = await new Inline(this, 'quoted', subbedText, {
1799
+ type,
1800
+ id,
1801
+ attributes,
1802
+ }).convert()
1803
+ }
1804
+ return subbedText.includes(PASS_START)
1805
+ ? this.restorePassthroughs(subbedText)
1806
+ : subbedText
1807
+ } else {
1808
+ this.logger.error(`unresolved passthrough detected: ${text}`)
1809
+ return '??pass??'
1810
+ }
1811
+ })
1812
+ },
1813
+
1814
+ /**
1815
+ * Resolve the list of comma-delimited subs against the possible options.
1816
+ *
1817
+ * @param {string} subs
1818
+ * @param {'block'|'inline'} [type='block']
1819
+ * @param {string[]|null} [defaults=null]
1820
+ * @param {string|null} [subject=null]
1821
+ * @returns {string[]|undefined}
1822
+ */
1823
+ resolveSubs(subs, type = 'block', defaults = null, subject = null) {
1824
+ if (!subs || subs.length === 0) return undefined
1825
+ let candidates = null
1826
+ if (subs.includes(' ')) subs = subs.split(' ').join('')
1827
+ const modifiersPresent = SubModifierSniffRx.test(subs)
1828
+
1829
+ for (let key of subs.split(',')) {
1830
+ let modifierOperation = null
1831
+ if (modifiersPresent) {
1832
+ const first = key.charAt(0)
1833
+ if (first === '+') {
1834
+ modifierOperation = 'append'
1835
+ key = key.slice(1)
1836
+ } else if (first === '-') {
1837
+ modifierOperation = 'remove'
1838
+ key = key.slice(1)
1839
+ } else if (key.endsWith('+')) {
1840
+ modifierOperation = 'prepend'
1841
+ key = key.slice(0, -1)
1842
+ }
1843
+ }
1844
+
1845
+ let resolvedKeys
1846
+ if (type === 'inline' && (key === 'verbatim' || key === 'v')) {
1847
+ resolvedKeys = BASIC_SUBS
1848
+ } else if (key in SUB_GROUPS) {
1849
+ resolvedKeys = SUB_GROUPS[key]
1850
+ } else if (type === 'inline' && key.length === 1 && key in SUB_HINTS) {
1851
+ const resolvedKey = SUB_HINTS[key]
1852
+ resolvedKeys = SUB_GROUPS[resolvedKey] || [resolvedKey]
1853
+ } else {
1854
+ resolvedKeys = [key]
1855
+ }
1856
+
1857
+ if (modifierOperation) {
1858
+ candidates = candidates ?? (defaults ? [...defaults] : [])
1859
+ switch (modifierOperation) {
1860
+ case 'append':
1861
+ candidates = [...candidates, ...resolvedKeys]
1862
+ break
1863
+ case 'prepend':
1864
+ candidates = [...resolvedKeys, ...candidates]
1865
+ break
1866
+ case 'remove':
1867
+ candidates = arrayDiff(candidates, resolvedKeys)
1868
+ break
1869
+ }
1870
+ } else {
1871
+ candidates = candidates ?? []
1872
+ candidates = [...candidates, ...resolvedKeys]
1873
+ }
1874
+ }
1875
+
1876
+ if (!candidates) return undefined
1877
+
1878
+ // weed out invalid options and remove duplicates (order preserved; first occurrence wins)
1879
+ const resolved = arrayIntersect(candidates, SUB_OPTIONS[type])
1880
+ const invalid = arrayDiff(candidates, resolved)
1881
+ if (invalid.length > 0) {
1882
+ this.logger.warn(
1883
+ `invalid substitution type${invalid.length > 1 ? 's' : ''}${subject ? ' for ' : ''}${subject || ''}: ${invalid.join(', ')}`
1884
+ )
1885
+ }
1886
+ return resolved
1887
+ },
1888
+
1889
+ /** Call resolveSubs for the 'block' type. */
1890
+ resolveBlockSubs(subs, defaults, subject) {
1891
+ return this.resolveSubs(subs, 'block', defaults, subject)
1892
+ },
1893
+
1894
+ /** Call resolveSubs for the 'inline' type with subject set as passthrough macro. */
1895
+ resolvePassSubs(subs, subject = 'passthrough macro') {
1896
+ return this.resolveSubs(subs, 'inline', null, subject)
1897
+ },
1898
+
1899
+ /**
1900
+ * Expand all groups in the subs list and return.
1901
+ *
1902
+ * @param {string|string[]} subs
1903
+ * @param {string|null} [subject=null]
1904
+ * @returns {string[]|null}
1905
+ */
1906
+ expandSubs(subs, subject = null) {
1907
+ if (typeof subs === 'string') {
1908
+ // subs is a single key name
1909
+ if (subs === 'none') return null
1910
+ return SUB_GROUPS[subs] || [subs]
1911
+ } else if (Array.isArray(subs)) {
1912
+ const expandedSubs = []
1913
+ for (const key of subs) {
1914
+ if (key !== 'none') {
1915
+ const subGroup = SUB_GROUPS[key]
1916
+ if (subGroup) {
1917
+ expandedSubs.push(...subGroup)
1918
+ } else {
1919
+ expandedSubs.push(key)
1920
+ }
1921
+ }
1922
+ }
1923
+ return expandedSubs.length === 0 ? null : expandedSubs
1924
+ } else {
1925
+ return this.resolveSubs(subs, 'inline', null, subject)
1926
+ }
1927
+ },
1928
+
1929
+ /**
1930
+ * Commit the requested substitutions to this block.
1931
+ * Looks for an attribute named "subs". If present, resolves substitutions.
1932
+ */
1933
+ commitSubs() {
1934
+ let defaultSubs = this.defaultSubs
1935
+ if (!defaultSubs) {
1936
+ switch (this.contentModel) {
1937
+ case 'simple':
1938
+ defaultSubs = NORMAL_SUBS
1939
+ break
1940
+ case 'verbatim':
1941
+ defaultSubs = this.context === 'verse' ? NORMAL_SUBS : VERBATIM_SUBS
1942
+ break
1943
+ case 'raw':
1944
+ defaultSubs = this.context === 'stem' ? BASIC_SUBS : NO_SUBS
1945
+ break
1946
+ default:
1947
+ return this.subs
1948
+ }
1949
+ }
1950
+
1951
+ const customSubs = this.attributes.subs
1952
+ if (customSubs) {
1953
+ this.subs =
1954
+ this.resolveBlockSubs(customSubs, defaultSubs, this.context) || []
1955
+ } else {
1956
+ this.subs = [...defaultSubs]
1957
+ }
1958
+
1959
+ if (
1960
+ this.context === 'listing' &&
1961
+ this.style === 'source' &&
1962
+ this.document.syntaxHighlighter?.handlesHighlighting()
1963
+ ) {
1964
+ const idx = this.subs.indexOf('specialcharacters')
1965
+ if (idx !== -1) this.subs[idx] = 'highlight'
1966
+ }
1967
+
1968
+ return null
1969
+ },
1970
+
1971
+ /**
1972
+ * Parse attributes in name or name=value format from a comma-separated String.
1973
+ *
1974
+ * @param {string} attrlist
1975
+ * @param {string[]} [posattrs=[]]
1976
+ * @param {Object} [opts={}]
1977
+ * @returns {Object}
1978
+ */
1979
+ async parseAttributes(attrlist, posattrs = [], opts = {}) {
1980
+ if (!attrlist || attrlist.length === 0) return {}
1981
+ if (opts.unescapeInput) attrlist = this.normalizeText(attrlist, true, true)
1982
+ if ((opts.subInput || opts.sub_input) && attrlist.includes(ATTR_REF_HEAD)) {
1983
+ attrlist = this.document.subAttributes(attrlist)
1984
+ }
1985
+ const block = opts.subResult || opts.sub_result ? this : null
1986
+ const al = new AttributeList(attrlist, block)
1987
+ if (opts.into) {
1988
+ return al.parseInto(opts.into, posattrs)
1989
+ }
1990
+ return al.parse(posattrs)
1991
+ },
1992
+
1993
+ // ── Private methods ────────────────────────────────────────────────────────
1994
+
1995
+ async extractAttributesFromText(text, defaultText = null) {
1996
+ const attrlist = text.includes(LF) ? text.split(LF).join(' ') : text
1997
+ const attrs = await new AttributeList(attrlist, this).parse()
1998
+ const resolvedText = attrs[1]
1999
+ if (resolvedText != null) {
2000
+ if (resolvedText === attrlist) {
2001
+ Object.keys(attrs).forEach((k) => {
2002
+ delete attrs[k]
2003
+ })
2004
+ return [text, attrs]
2005
+ }
2006
+ return [resolvedText, attrs]
2007
+ }
2008
+ return [defaultText, attrs]
2009
+ },
2010
+
2011
+ extractCallouts(source) {
2012
+ const calloutMarks = {}
2013
+ let autonum = 0
2014
+ let lineno = 0
2015
+ let lastLineno = null
2016
+ const calloutRx = this.hasAttribute('line-comment')
2017
+ ? CalloutExtractRxMap[this.getAttribute('line-comment')]
2018
+ : CalloutExtractRx
2019
+
2020
+ const lines = source.split(LF).map((line) => {
2021
+ lineno++
2022
+ return line.replace(globalRx(calloutRx), (match, p1, p2, p3, p4) => {
2023
+ if (p2) {
2024
+ return match.replace(RS, '')
2025
+ }
2026
+ const guard = p1 || (p3 === '--' ? ['<!--', '-->'] : null)
2027
+ const numeral = p4 === '.' ? String(++autonum) : p4
2028
+ ;(calloutMarks[lineno] = calloutMarks[lineno] || []).push([
2029
+ guard,
2030
+ numeral,
2031
+ ])
2032
+ lastLineno = lineno
2033
+ return ''
2034
+ })
2035
+ })
2036
+
2037
+ let result = lines.join(LF)
2038
+ if (lastLineno !== null) {
2039
+ if (lastLineno === lineno) result = `${result}${LF}`
2040
+ } else {
2041
+ return [result, null]
2042
+ }
2043
+ return [result, calloutMarks]
2044
+ },
2045
+
2046
+ async restoreCallouts(source, calloutMarks, sourceOffset = null) {
2047
+ let preamble = ''
2048
+ if (sourceOffset !== null) {
2049
+ preamble = source.slice(0, sourceOffset)
2050
+ source = source.slice(sourceOffset)
2051
+ }
2052
+ let lineno = 0
2053
+ const result = await Promise.all(
2054
+ source.split(LF).map(async (line) => {
2055
+ const conums = calloutMarks[++lineno]
2056
+ if (conums) {
2057
+ delete calloutMarks[lineno]
2058
+ if (conums.length === 1) {
2059
+ const [guard, numeral] = conums[0]
2060
+ return `${line}${await new Inline(this, 'callout', numeral, {
2061
+ id: this.document.callouts.readNextId(),
2062
+ attributes: { guard },
2063
+ }).convert()}`
2064
+ } else {
2065
+ const converted = await Promise.all(
2066
+ conums.map(([guard, numeral]) =>
2067
+ new Inline(this, 'callout', numeral, {
2068
+ id: this.document.callouts.readNextId(),
2069
+ attributes: { guard },
2070
+ }).convert()
2071
+ )
2072
+ )
2073
+ return `${line}${converted.join(' ')}`
2074
+ }
2075
+ }
2076
+ return line
2077
+ })
2078
+ )
2079
+ return preamble + result.join(LF)
2080
+ },
2081
+
2082
+ async convertQuotedText(args, type, scope) {
2083
+ // args: [fullMatch, group1, group2, ...]
2084
+ const fullMatch = args[0]
2085
+ if (fullMatch.startsWith(RS)) {
2086
+ if (scope === 'constrained') {
2087
+ const attrs = args[2]
2088
+ if (attrs) {
2089
+ return `[${attrs}]${await new Inline(this, 'quoted', args[3], { type }).convert()}`
2090
+ }
2091
+ }
2092
+ return fullMatch.slice(1)
2093
+ }
2094
+
2095
+ if (scope === 'constrained') {
2096
+ const attrlist = args[2]
2097
+ let id, attributes
2098
+ if (attrlist) {
2099
+ attributes = this.parseQuotedTextAttributes(attrlist)
2100
+ id = attributes.id
2101
+ if (type === 'mark') type = 'unquoted'
2102
+ }
2103
+ return `${args[1] || ''}${await new Inline(this, 'quoted', args[3], { type, id, attributes }).convert()}`
2104
+ } else {
2105
+ const attrlist = args[1]
2106
+ let id, attributes
2107
+ if (attrlist) {
2108
+ attributes = this.parseQuotedTextAttributes(attrlist)
2109
+ id = attributes.id
2110
+ if (type === 'mark') type = 'unquoted'
2111
+ }
2112
+ return new Inline(this, 'quoted', args[2], {
2113
+ type,
2114
+ id,
2115
+ attributes,
2116
+ }).convert()
2117
+ }
2118
+ },
2119
+
2120
+ doReplacement(match, replacement, restore) {
2121
+ const captured = match[0]
2122
+ if (captured.includes(RS)) {
2123
+ return captured.replace(RS, '')
2124
+ }
2125
+ switch (restore) {
2126
+ case 'none':
2127
+ return replacement
2128
+ case 'bounding':
2129
+ return match[1] + replacement + match[2]
2130
+ default: // 'leading'
2131
+ return match[1] + replacement
2132
+ }
2133
+ },
2134
+
2135
+ /** Inserts text into a formatted text enclosure (sprintf). */
2136
+ subPlaceholder(format, ...args) {
2137
+ let i = 0
2138
+ return format.replace(/%s/g, () => String(args[i++] ?? ''))
2139
+ },
2140
+
2141
+ parseQuotedTextAttributes(str) {
2142
+ if (str.includes(ATTR_REF_HEAD)) str = this.subAttributes(str)
2143
+ // for compliance, only consider first positional attribute
2144
+ if (str.includes(',')) str = str.slice(0, str.indexOf(','))
2145
+ str = str.trim()
2146
+ if (!str) return {}
2147
+ if (
2148
+ (str.startsWith('.') || str.startsWith('#')) &&
2149
+ Compliance.shorthandPropertySyntax
2150
+ ) {
2151
+ const [before, , after] = partition(str, '#')
2152
+ const attrs = {}
2153
+ if (!after) {
2154
+ if (before.length > 1)
2155
+ attrs.role = before.slice(1).split('.').join(' ').trimStart()
2156
+ } else {
2157
+ const [id, , roles] = partition(after, '.')
2158
+ if (id) attrs.id = id
2159
+ if (!roles) {
2160
+ if (before.length > 1)
2161
+ attrs.role = before.slice(1).split('.').join(' ').trimStart()
2162
+ } else if (before.length > 1) {
2163
+ attrs.role = `${before}.${roles}`
2164
+ .slice(1)
2165
+ .split('.')
2166
+ .join(' ')
2167
+ .trimStart()
2168
+ } else {
2169
+ attrs.role = roles.split('.').join(' ')
2170
+ }
2171
+ }
2172
+ return attrs
2173
+ }
2174
+ return { role: str }
2175
+ },
2176
+
2177
+ normalizeText(
2178
+ text,
2179
+ normalizeWhitespace = null,
2180
+ unescapeClosingSquareBrackets = null
2181
+ ) {
2182
+ if (text && text.length > 0) {
2183
+ if (normalizeWhitespace) text = text.trim().split(LF).join(' ')
2184
+ if (unescapeClosingSquareBrackets && text.includes(R_SB)) {
2185
+ text = text.split(ESC_R_SB).join(R_SB)
2186
+ }
2187
+ }
2188
+ return text
2189
+ },
2190
+
2191
+ splitSimpleCsv(str) {
2192
+ if (!str || str.length === 0) return []
2193
+ if (str.includes('"')) {
2194
+ const values = []
2195
+ let accum = ''
2196
+ let quoteOpen = false
2197
+ for (const c of str) {
2198
+ if (c === ',') {
2199
+ if (quoteOpen) {
2200
+ accum += c
2201
+ } else {
2202
+ values.push(accum.trim())
2203
+ accum = ''
2204
+ }
2205
+ } else if (c === '"') {
2206
+ quoteOpen = !quoteOpen
2207
+ } else {
2208
+ accum += c
2209
+ }
2210
+ }
2211
+ values.push(accum.trim())
2212
+ return values
2213
+ }
2214
+ return str.split(',').map((item) => item.trim())
2215
+ },
2216
+ }
2217
+
2218
+ export {
2219
+ BASIC_SUBS,
2220
+ HEADER_SUBS,
2221
+ NO_SUBS,
2222
+ NORMAL_SUBS,
2223
+ REFTEXT_SUBS,
2224
+ VERBATIM_SUBS,
2225
+ SUB_GROUPS,
2226
+ SUB_HINTS,
2227
+ SUB_OPTIONS,
2228
+ CAN,
2229
+ DEL,
2230
+ PASS_START,
2231
+ PASS_END,
2232
+ PASS_SLOT_RX,
2233
+ HIGHLIGHTED_PASS_SLOT_RX,
2234
+ RS,
2235
+ R_SB,
2236
+ ESC_R_SB,
2237
+ PLUS,
2238
+ SPECIAL_CHARS_RX,
2239
+ SPECIAL_CHARS_TR,
2240
+ QUOTED_TEXT_SNIFF_RX,
2241
+ }