@asciidoctor/core 3.0.3 → 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,849 @@
1
+ /** @import { Cursor } from './reader.js' */
2
+
3
+ // ESM conversion of abstract_block.rb
4
+ //
5
+ // Ruby-to-JavaScript notes:
6
+ // - AbstractBlock extends AbstractNode (class inheritance, not mixin).
7
+ // - Ruby symbols (:compound, :section, …) are represented as plain strings.
8
+ // - attr_reader / attr_writer / attr_accessor are plain instance properties.
9
+ // - title has a getter/setter pair because the getter memoises the result of
10
+ // applying substitutions; the setter clears that cache.
11
+ // - caption has a getter/setter pair because the getter has special logic for
12
+ // admonition blocks (returns 'textlabel' attribute instead).
13
+ // - The context= setter in Ruby (def context= context) is exposed as setContext()
14
+ // rather than a JS set accessor, to avoid getter/setter shadowing issues with
15
+ // AbstractNode's plain this.context property.
16
+ // - Ruby's number / number= (deprecated section numeral accessors) become
17
+ // get number() / set number() JS accessors.
18
+ // - The find_by Ruby block argument becomes a filter function (null if omitted).
19
+ // - Ruby's StopIteration mechanism is replicated with a private StopIteration
20
+ // class thrown and caught only within findBy / #findByInternal.
21
+ // - find_by_internal is protected in Ruby; in JS it is a private method (#).
22
+ // Because JS private methods are accessible across instances of the same class
23
+ // from within the class body, recursive calls on child blocks still work.
24
+ // - Ruby's Array#flatten (used for dlist blocks) → Array#flat().
25
+ // - nil_or_empty? → falsy check (!val) since both null and '' are falsy in JS.
26
+ // - String#chomp(sep) → str.endsWith(sep) ? str.slice(0, -sep.length) : str.
27
+ // - Substitutors methods referenced: applyTitleSubs, subSpecialchars,
28
+ // subReplacements, subQuotes, subPlaceholder (mixed in externally).
29
+ // - document.incrementAndStoreCounter / document.counter translate
30
+ // Ruby's increment_and_store_counter / counter.
31
+
32
+ import { AbstractNode } from './abstract_node.js'
33
+ import {
34
+ LF,
35
+ CAPTION_ATTRIBUTE_NAMES,
36
+ ORDERED_LIST_KEYWORDS,
37
+ ReplaceableTextRx,
38
+ } from './constants.js'
39
+ import { intToRoman } from './helpers.js'
40
+
41
+ /** Used as a sentinel to abort findBy traversal early (mirrors Ruby StopIteration). */
42
+ class StopIteration extends Error {}
43
+
44
+ /**
45
+ * @template {string | any[]} [TContent=string]
46
+ * @abstract
47
+ */
48
+ export class AbstractBlock extends AbstractNode {
49
+ /** @type {string|null} */
50
+ #title = null
51
+ /** @type {string|null} */
52
+ #convertedTitle = null
53
+ /** @type {string|null} */
54
+ #caption = null
55
+ /** @type {string[]} */
56
+ subs
57
+ /** @type {string[]|null} */
58
+ defaultSubs
59
+ /** @type {string|number|null} */
60
+ numeral
61
+ /** @type {Cursor|null} */
62
+ sourceLocation
63
+ /** @internal */
64
+ _nextSectionIndex
65
+ /** @internal */
66
+ _nextSectionOrdinal
67
+
68
+ /**
69
+ * @param {AbstractBlock} parent
70
+ * @param {string} context
71
+ * @param {object} [opts={}]
72
+ */
73
+ constructor(parent, context, opts = {}) {
74
+ super(parent, context, opts)
75
+ /**
76
+ * Describes the type of content this block accepts and how it should be converted. Acceptable values are:
77
+ * - `compound` - this block contains other blocks
78
+ * - `simple` - this block holds a paragraph of prose that receives normal substitutions
79
+ * - `verbatim` - this block holds verbatim text (displayed "as is") that receives verbatim substitutions
80
+ * - `raw` - this block holds unprocessed content passed directly to the output with no substitutions applied
81
+ * - `empty` - this block has no content
82
+ * @type {string}
83
+ */
84
+ this.contentModel = 'compound'
85
+ /**
86
+ * Array of {@link AbstractBlock} child blocks for this block. Only applies if content model is `compound`.
87
+ * @type {AbstractBlock[]}
88
+ */
89
+ this.blocks = []
90
+ this.subs = []
91
+ this.#title = null
92
+ this.#caption = null
93
+ this.numeral = null
94
+ this.style = null
95
+ this.defaultSubs = null
96
+ this.sourceLocation = null
97
+ if (context === 'document' || context === 'section') {
98
+ this.level = 0
99
+ this._nextSectionIndex = 0
100
+ this._nextSectionOrdinal = 1
101
+ } else if (parent instanceof AbstractBlock) {
102
+ this.level = parent.level
103
+ } else {
104
+ this.level = null
105
+ }
106
+ }
107
+
108
+ isBlock() {
109
+ return true
110
+ }
111
+ isInline() {
112
+ return false
113
+ }
114
+
115
+ /**
116
+ * Get the String title of this block with title substitutions applied.
117
+ * The result is pre-computed during Document.parse() via precomputeTitle().
118
+ * Falls back to applyHeaderSubs (sync) if precomputeTitle() has not been called yet
119
+ * (e.g. when a title is set via the API after parsing).
120
+ * @returns {string|null} the converted String title, or null if the source title is falsy.
121
+ */
122
+ get title() {
123
+ if (this.#convertedTitle != null) return this.#convertedTitle
124
+ if (this.#title == null) return null
125
+ // Pre-computation hasn't run (title set after parse, or parse not yet done).
126
+ // Apply the synchronous header subs (specialcharacters + attributes) as a best-effort.
127
+ return this.applyHeaderSubs(this.#title)
128
+ }
129
+
130
+ /**
131
+ * Pre-compute the converted title asynchronously.
132
+ * Called during Document.parse() so the synchronous getter works during conversion.
133
+ * Re-entrant calls (circular title references) are detected via _computingTitle and
134
+ * silently skipped so that {@link Section.xreftext()} can return null (→ "[refid]" fallback).
135
+ * @returns {Promise<void>}
136
+ */
137
+ async precomputeTitle() {
138
+ if (this.#title && this.#convertedTitle == null && !this._computingTitle) {
139
+ this._computingTitle = true
140
+ try {
141
+ this.#convertedTitle = await this.applyTitleSubs(this.#title)
142
+ } finally {
143
+ this._computingTitle = false
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * @internal Get the raw (unsubstituted) title as set by the parser.
150
+ * @returns {string|null}
151
+ */
152
+ get rawTitle() {
153
+ return this.#title
154
+ }
155
+
156
+ /**
157
+ * @internal Get the title with only attribute substitutions applied (no specialchars).
158
+ * @note no longer used for section ID generation (parser now calls applyTitleSubs to match
159
+ * Ruby's behaviour). Kept for other callers that need a lightweight sync substitution.
160
+ * @returns {string|null}
161
+ */
162
+ get attrSubstitutedTitle() {
163
+ const raw = this.#title
164
+ if (raw == null) return null
165
+ return raw.includes('{') ? this.subAttributes(raw) : raw
166
+ }
167
+
168
+ /**
169
+ * Set the String block title (clears the memoised converted title).
170
+ * @param {string|null} val
171
+ */
172
+ set title(val) {
173
+ this.#convertedTitle = null
174
+ this.#title = val
175
+ }
176
+
177
+ /**
178
+ * Check whether the title of this block is defined.
179
+ * @returns {boolean}
180
+ */
181
+ hasTitle() {
182
+ return !!this.#title
183
+ }
184
+
185
+ /**
186
+ * Get the caption for this block.
187
+ * For admonition blocks, returns the 'textlabel' attribute instead.
188
+ * @returns {string|null}
189
+ */
190
+ get caption() {
191
+ return this.context === 'admonition'
192
+ ? this.attributes.textlabel
193
+ : this.#caption
194
+ }
195
+
196
+ /**
197
+ * Set the caption for this block.
198
+ * @param {string|null} val
199
+ */
200
+ set caption(val) {
201
+ this.#caption = val
202
+ }
203
+
204
+ /**
205
+ * Get the source file where this block started.
206
+ * @returns {string|null}
207
+ */
208
+ get file() {
209
+ return this.sourceLocation?.file
210
+ }
211
+
212
+ /**
213
+ * Get the source line number where this block started.
214
+ * @returns {number|null}
215
+ */
216
+ get lineno() {
217
+ return this.sourceLocation?.lineno
218
+ }
219
+
220
+ /**
221
+ * Update the context of this block, also updating the node name.
222
+ * @param {string} context - The String context to assign to this block.
223
+ */
224
+ setContext(context) {
225
+ this.context = context
226
+ this.nodeName = String(context)
227
+ }
228
+
229
+ /**
230
+ * @deprecated Get/set the numeral of this section as an integer when possible.
231
+ * @returns {number|string}
232
+ */
233
+ get number() {
234
+ const n = parseInt(this.numeral, 10)
235
+ return String(n) === String(this.numeral) ? n : this.numeral
236
+ }
237
+
238
+ /**
239
+ * @deprecated
240
+ * @param {number|string} val
241
+ */
242
+ set number(val) {
243
+ this.numeral = String(val)
244
+ }
245
+
246
+ /**
247
+ * Convert this block and return the converted String content.
248
+ * @returns {Promise<string>} the result of the converter.
249
+ */
250
+ async convert() {
251
+ this.document.playbackAttributes(this.attributes)
252
+ return this.converter.convert(this)
253
+ }
254
+
255
+ /** @deprecated Use convert() instead. */
256
+ render() {
257
+ return this.convert()
258
+ }
259
+
260
+ /**
261
+ * Get the converted result of all child blocks joined with a newline.
262
+ * @returns {Promise<TContent>}
263
+ */
264
+ async content() {
265
+ const results = []
266
+ for (const b of this.blocks) results.push(await b.convert())
267
+ return results.join(LF)
268
+ }
269
+
270
+ /**
271
+ * Alias for the content method — mirrors the core API.
272
+ * @returns {Promise<TContent>}
273
+ */
274
+ getContent() {
275
+ return this.content()
276
+ }
277
+
278
+ /**
279
+ * Append a content block to this block's list of blocks.
280
+ * @param {AbstractBlock} block - The new child block.
281
+ * @returns {AbstractBlock} this block (enables chaining).
282
+ */
283
+ append(block) {
284
+ if (block.getParent() !== this) block.parent = this
285
+ this.blocks.push(block)
286
+ return this
287
+ }
288
+
289
+ /**
290
+ * Determine whether this block contains block content.
291
+ * @returns {boolean}
292
+ */
293
+ hasBlocks() {
294
+ return this.blocks.length > 0
295
+ }
296
+
297
+ /**
298
+ * Check whether this block has any child Section objects.
299
+ * Overridden by Document and Section.
300
+ * @returns {boolean}
301
+ */
302
+ hasSections() {
303
+ return false
304
+ }
305
+
306
+ /**
307
+ * Get the child Section objects of this block.
308
+ * Only applies to Document and Section instances.
309
+ * @returns {AbstractBlock[]} array of Section objects (may be empty).
310
+ */
311
+ sections() {
312
+ return this.blocks.filter((b) => b.context === 'section')
313
+ }
314
+
315
+ /**
316
+ * Get the converted alt text for this block image.
317
+ * @returns {string} string with XML special character and replacement substitutions applied.
318
+ */
319
+ alt() {
320
+ const text = this.attributes.alt
321
+ if (text) {
322
+ if (text === this.attributes['default-alt'])
323
+ return this.subSpecialchars(text)
324
+ const escaped = this.subSpecialchars(text)
325
+ return ReplaceableTextRx.test(escaped)
326
+ ? this.subReplacements(escaped)
327
+ : escaped
328
+ }
329
+ return ''
330
+ }
331
+
332
+ /**
333
+ * Get the converted alt text for this block image (alias of alt).
334
+ * @returns {string}
335
+ */
336
+ getAlt() {
337
+ return this.alt()
338
+ }
339
+
340
+ /**
341
+ * Get the converted title prefixed with the caption.
342
+ * @returns {string} the captioned title.
343
+ */
344
+ captionedTitle() {
345
+ return `${this.caption || ''}${this.title || ''}`
346
+ }
347
+
348
+ /**
349
+ * Get the list marker keyword for the specified list type.
350
+ * @param {string|null} [listType=null] - The String list type (default: this.style).
351
+ * @returns {string|undefined} the single-character String keyword for the list marker.
352
+ */
353
+ listMarkerKeyword(listType = null) {
354
+ return ORDERED_LIST_KEYWORDS[listType || this.style]
355
+ }
356
+
357
+ /**
358
+ * Check whether the specified substitution is enabled for this block.
359
+ * @param {string} name - The String substitution name.
360
+ * @returns {boolean}
361
+ */
362
+ hasSub(name) {
363
+ return this.subs.includes(name)
364
+ }
365
+
366
+ /**
367
+ * Remove a substitution from this block.
368
+ * @param {string} name - The String substitution name to remove.
369
+ */
370
+ removeSub(name) {
371
+ const idx = this.subs.indexOf(name)
372
+ if (idx >= 0) this.subs.splice(idx, 1)
373
+ }
374
+
375
+ /**
376
+ * Alias for {@link getXrefText}.
377
+ * @param {string|null} [xrefstyle=null] - Optional String style: 'full', 'short', or 'basic'.
378
+ * @returns {Promise<string|null>} the xreftext, or null.
379
+ * @see {getXrefText}
380
+ */
381
+ async xreftext(xrefstyle = null) {
382
+ const val = this.reftext
383
+ if (val && val.length > 0) return val
384
+ if (xrefstyle && this.#title && this.#caption) {
385
+ if (xrefstyle === 'full') {
386
+ const quoteTemplate = this.document.compatMode ? "``%s''" : '"`%s`"'
387
+ const quotedTitle = this.subPlaceholder(
388
+ await this.subQuotes(quoteTemplate),
389
+ this.title
390
+ )
391
+ if (this.numeral) {
392
+ const captionAttrName = CAPTION_ATTRIBUTE_NAMES[this.context]
393
+ if (captionAttrName) {
394
+ const prefix = this.document.attributes[captionAttrName]
395
+ if (prefix) return `${prefix} ${this.numeral}, ${quotedTitle}`
396
+ }
397
+ }
398
+ const cap = this.#caption
399
+ return `${cap.endsWith('. ') ? cap.slice(0, -2) : cap}, ${quotedTitle}`
400
+ } else if (xrefstyle === 'short') {
401
+ if (this.numeral) {
402
+ const captionAttrName = CAPTION_ATTRIBUTE_NAMES[this.context]
403
+ if (captionAttrName) {
404
+ const prefix = this.document.attributes[captionAttrName]
405
+ if (prefix) return `${prefix} ${this.numeral}`
406
+ }
407
+ }
408
+ const cap = this.#caption
409
+ return cap.endsWith('. ') ? cap.slice(0, -2) : cap
410
+ }
411
+ }
412
+ return this.title
413
+ }
414
+
415
+ /**
416
+ * Generate and assign a caption to this block if not already assigned.
417
+ * If the block has a title and a caption prefix is available, builds a caption
418
+ * from the prefix and a counter, then stores it.
419
+ * @param {string|null} [value=null] - The String caption to assign, or null to derive from document attributes.
420
+ * @param {string} [captionContext=this.context] - The String context used to look up caption attributes.
421
+ */
422
+ assignCaption(value = null, captionContext = this.context) {
423
+ // In Ruby, empty string is truthy; use != null to replicate that semantics.
424
+ if (this.#caption != null || !this.#title) return
425
+ const globalCaption = this.document.attributes.caption
426
+ // Explicit value (even '') or a global :caption: attribute (even empty) takes precedence and
427
+ // suppresses auto-numbering, matching Ruby's behaviour where any truthy assignment wins.
428
+ if (value != null || globalCaption != null) {
429
+ this.#caption = value != null ? value : globalCaption
430
+ } else {
431
+ const attrName = CAPTION_ATTRIBUTE_NAMES[captionContext]
432
+ if (attrName) {
433
+ const prefix = this.document.attributes[attrName]
434
+ if (prefix) {
435
+ this.numeral = this.document.incrementAndStoreCounter(
436
+ `${captionContext}-number`,
437
+ this
438
+ )
439
+ this.#caption = `${prefix} ${this.numeral}. `
440
+ }
441
+ }
442
+ }
443
+ }
444
+
445
+ /**
446
+ * @internal Assign the next index (0-based) and numeral (1-based) to the section.
447
+ * @param {AbstractBlock} section - The Section to which to assign the next index and numeral.
448
+ */
449
+ assignNumeral(section) {
450
+ section.index = this._nextSectionIndex
451
+ this._nextSectionIndex = section.index + 1
452
+ const like = section.numbered
453
+ if (like) {
454
+ const sectname = section.sectname
455
+ if (sectname === 'appendix') {
456
+ section.numeral = this.document.counter('appendix-number', 'A')
457
+ const captionAttr = this.document.attributes['appendix-caption']
458
+ section.caption = captionAttr
459
+ ? `${captionAttr} ${section.numeral}: `
460
+ : `${section.numeral}. `
461
+ } else if (sectname === 'chapter' || like === 'chapter') {
462
+ section.numeral = String(this.document.counter('chapter-number', 1))
463
+ } else {
464
+ section.numeral =
465
+ sectname === 'part'
466
+ ? intToRoman(this._nextSectionOrdinal)
467
+ : String(this._nextSectionOrdinal)
468
+ this._nextSectionOrdinal++
469
+ }
470
+ }
471
+ }
472
+
473
+ /**
474
+ * @internal Reassign 0-based section indexes for all descendant sections.
475
+ * Must be called after removing child sections to keep internal counters correct.
476
+ */
477
+ reindexSections() {
478
+ this._nextSectionIndex = 0
479
+ this._nextSectionOrdinal = 1
480
+ for (const block of this.blocks) {
481
+ if (block.context === 'section') {
482
+ this.assignNumeral(block)
483
+ block.reindexSections()
484
+ }
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Walk the document tree and find all block-level nodes that match
490
+ * the selector and optional filter function.
491
+ *
492
+ * The selector is a plain object whose keys narrow the search:
493
+ * - `context` {string} — node context (e.g. `'section'`, `'listing'`, `'paragraph'`, `'image'`)
494
+ * - `style` {string} — block style (e.g. `'source'`, `'NOTE'`)
495
+ * - `role` {string} — a CSS role that must appear in the node's role list
496
+ * - `id` {string} — exact node id; stops traversal after the first match
497
+ * - `traverseDocuments` {boolean} — when `true`, recurse into AsciiDoc table cells
498
+ *
499
+ * The optional filter function receives each candidate node and must return:
500
+ * - a truthy value (or `true`) → include the node
501
+ * - `'prune'` → include the node but do **not** recurse into its children
502
+ * - `'reject'` → skip the node and its children
503
+ * - `'stop'` → include the node (if it matched) and stop the entire traversal
504
+ *
505
+ * @param {Object} [selector={}] - Selector criteria object.
506
+ * @param {Function|null} [filter=null] - Per-node filter callback.
507
+ * @returns {AbstractBlock[]} array of matching block-level nodes.
508
+ *
509
+ * @example <caption>All source listing blocks</caption>
510
+ * const listings = doc.findBy({ context: 'listing', style: 'source' })
511
+ *
512
+ * @example <caption>All sections up to level 2</caption>
513
+ * const sections = doc.findBy({ context: 'section' }, (node) => node.level <= 2 || 'prune')
514
+ *
515
+ * @example <caption>Find a block by id</caption>
516
+ * const [block] = doc.findBy({ id: 'my-anchor' })
517
+ *
518
+ * @example <caption>All image blocks including those inside AsciiDoc table cells</caption>
519
+ * const images = doc.findBy({ context: 'image', traverseDocuments: true })
520
+ */
521
+ findBy(selector = {}, filter = null) {
522
+ const result = []
523
+ // Normalise: if selector is not a plain object, treat it as an empty selector.
524
+ const normSelector =
525
+ selector && typeof selector === 'object' && !Array.isArray(selector)
526
+ ? selector
527
+ : {}
528
+ // Normalise: filter must be a function; ignore string/non-function values (mirrors core API).
529
+ const normFilter = typeof filter === 'function' ? filter : null
530
+ try {
531
+ this.#findByInternal(normSelector, result, normFilter)
532
+ } catch (e) {
533
+ if (!(e instanceof StopIteration)) throw e
534
+ }
535
+ return result
536
+ }
537
+
538
+ /** Alias for {@link findBy}. */
539
+ query(selector = {}, filter = null) {
540
+ return this.findBy(selector, filter)
541
+ }
542
+
543
+ /**
544
+ * Move to the next adjacent block in document order.
545
+ * If the current block is the last item in a list, returns the following
546
+ * sibling of the list block.
547
+ * @returns {AbstractBlock|null} the next AbstractBlock, or null.
548
+ */
549
+ nextAdjacentBlock() {
550
+ if (this.context === 'document') return null
551
+ const p = this.getParent()
552
+ if (p.context === 'dlist' && this.context === 'list_item') {
553
+ const idx = p
554
+ .getItems()
555
+ .findIndex(([terms, desc]) => terms.includes(this) || desc === this)
556
+ const sib = p.getItems()[idx + 1]
557
+ return sib ? sib : p.nextAdjacentBlock()
558
+ }
559
+ const idx = p.blocks.indexOf(this)
560
+ const sib = p.blocks[idx + 1]
561
+ return sib ? sib : p.nextAdjacentBlock()
562
+ }
563
+
564
+ /** @private Core traversal logic for findBy. Throws StopIteration for early exit. */
565
+ #findByInternal(selector, result, filter) {
566
+ const contextSelector = selector.context ?? null
567
+ const anyContext = !contextSelector
568
+ const styleSelector = selector.style ?? null
569
+ const roleSelector = selector.role ?? null
570
+ const idSelector = selector.id ?? null
571
+
572
+ if (
573
+ (anyContext || contextSelector === this.context) &&
574
+ (!styleSelector || styleSelector === this.style) &&
575
+ (!roleSelector || this.hasRole(roleSelector)) &&
576
+ (!idSelector || idSelector === this.id)
577
+ ) {
578
+ if (filter) {
579
+ const verdict = filter(this)
580
+ if (verdict) {
581
+ if (verdict === 'prune') {
582
+ result.push(this)
583
+ if (idSelector) throw new StopIteration()
584
+ return result
585
+ } else if (verdict === 'reject') {
586
+ if (idSelector) throw new StopIteration()
587
+ return result
588
+ } else if (verdict === 'stop') {
589
+ throw new StopIteration()
590
+ } else {
591
+ result.push(this)
592
+ if (idSelector) throw new StopIteration()
593
+ }
594
+ } else if (idSelector) {
595
+ throw new StopIteration()
596
+ }
597
+ } else {
598
+ result.push(this)
599
+ if (idSelector) throw new StopIteration()
600
+ }
601
+ }
602
+
603
+ if (this.context === 'document') {
604
+ if (contextSelector !== 'document') {
605
+ // Process document header as a section if present
606
+ if (
607
+ this.hasHeader?.() &&
608
+ (anyContext || contextSelector === 'section')
609
+ ) {
610
+ this.header.#findByInternal(selector, result, filter)
611
+ }
612
+ for (const b of this.blocks) {
613
+ if (contextSelector === 'section' && b.context !== 'section') continue // optimisation
614
+ b.#findByInternal(selector, result, filter)
615
+ }
616
+ }
617
+ } else if (this.context === 'dlist') {
618
+ if (anyContext || contextSelector !== 'section') {
619
+ // optimisation
620
+ // NOTE dlist items can be null
621
+ for (const b of this.blocks.flat()) {
622
+ if (b) b.#findByInternal(selector, result, filter)
623
+ }
624
+ }
625
+ } else if (this.context === 'table') {
626
+ if (selector.traverseDocuments) {
627
+ for (const r of this.rows.head)
628
+ for (const c of r) c.#findByInternal(selector, result, filter)
629
+ const innerSelector =
630
+ contextSelector === 'inner_document'
631
+ ? { ...selector, context: 'document' }
632
+ : selector
633
+ for (const r of [...this.rows.body, ...this.rows.foot]) {
634
+ for (const c of r) {
635
+ c.#findByInternal(innerSelector, result, filter)
636
+ if (c.style === 'asciidoc')
637
+ c.innerDocument.#findByInternal(innerSelector, result, filter)
638
+ }
639
+ }
640
+ } else {
641
+ for (const r of [
642
+ ...this.rows.head,
643
+ ...this.rows.body,
644
+ ...this.rows.foot,
645
+ ]) {
646
+ for (const c of r) c.#findByInternal(selector, result, filter)
647
+ }
648
+ }
649
+ } else {
650
+ for (const b of this.blocks) {
651
+ if (contextSelector === 'section' && b.context !== 'section') continue // optimisation
652
+ b.#findByInternal(selector, result, filter)
653
+ }
654
+ }
655
+
656
+ return result
657
+ }
658
+
659
+ // ── JavaScript-style accessors ────────────────────────────────────────────────
660
+
661
+ /**
662
+ * Get the context (node type) of this block.
663
+ * @returns {string}
664
+ */
665
+ getContext() {
666
+ return this.context
667
+ }
668
+
669
+ /**
670
+ * Get the content model of this block.
671
+ * @returns {string}
672
+ */
673
+ getContentModel() {
674
+ return this.contentModel
675
+ }
676
+
677
+ /**
678
+ * Set the content model of this block.
679
+ * @param {string} val
680
+ */
681
+ setContentModel(val) {
682
+ this.contentModel = val
683
+ }
684
+
685
+ /**
686
+ * Get the node name of this block.
687
+ * @returns {string}
688
+ */
689
+ getNodeName() {
690
+ return this.nodeName
691
+ }
692
+
693
+ /**
694
+ * Get the child blocks of this block.
695
+ * @returns {AbstractBlock[]}
696
+ */
697
+ getBlocks() {
698
+ return this.blocks
699
+ }
700
+
701
+ /**
702
+ * Get the child Section blocks of this block.
703
+ * @returns {AbstractBlock[]}
704
+ */
705
+ getSections() {
706
+ return this.sections()
707
+ }
708
+
709
+ /**
710
+ * Get the title of this block with substitutions applied.
711
+ * @returns {string|null}
712
+ */
713
+ getTitle() {
714
+ return this.title
715
+ }
716
+
717
+ /**
718
+ * Set the raw title of this block.
719
+ * @param {string|null} val
720
+ */
721
+ setTitle(val) {
722
+ this.title = val ?? null
723
+ }
724
+
725
+ /**
726
+ * Get the caption of this block.
727
+ * @returns {string|undefined}
728
+ */
729
+ getCaption() {
730
+ return this.caption ?? undefined
731
+ }
732
+
733
+ /**
734
+ * Set the caption of this block.
735
+ * @param {string|null} val
736
+ */
737
+ setCaption(val) {
738
+ this.caption = val
739
+ }
740
+
741
+ /**
742
+ * Get the captioned title of this block.
743
+ * @returns {string}
744
+ */
745
+ getCaptionedTitle() {
746
+ return this.captionedTitle()
747
+ }
748
+
749
+ /**
750
+ * Get the style of this block.
751
+ * @returns {string|null}
752
+ */
753
+ getStyle() {
754
+ return this.style
755
+ }
756
+
757
+ /**
758
+ * Set the style of this block.
759
+ * @param {string|null} val
760
+ */
761
+ setStyle(val) {
762
+ this.style = val
763
+ }
764
+
765
+ /**
766
+ * Get the level of this block.
767
+ * @returns {number|null}
768
+ */
769
+ getLevel() {
770
+ return this.level
771
+ }
772
+
773
+ /**
774
+ * Set the level of this block.
775
+ * @param {number|null} val
776
+ */
777
+ setLevel(val) {
778
+ this.level = val
779
+ }
780
+
781
+ /**
782
+ * Get the source file where this block started.
783
+ * @returns {string|undefined} the file path, or undefined when sourcemap is disabled.
784
+ */
785
+ getFile() {
786
+ return this.file ?? undefined
787
+ }
788
+
789
+ /**
790
+ * Get the source line number where this block started.
791
+ * @returns {number|undefined} line number, or undefined when sourcemap is disabled.
792
+ */
793
+ getLineNumber() {
794
+ return this.lineno ?? undefined
795
+ }
796
+
797
+ /**
798
+ * Generate cross-reference text (xreftext) used to refer to this block.
799
+ * Uses the explicit reftext if set. For sections or captioned blocks (blocks
800
+ * with both a title and a caption), formats the text according to xrefstyle.
801
+ * Falls back to the title, or null if no title is available.
802
+ * @param {string|null} [xrefstyle=null] - Optional String style: 'full', 'short', or 'basic'.
803
+ * @returns {Promise<string|null>} the xreftext, or null.
804
+ */
805
+ async getXrefText(xrefstyle = null) {
806
+ return this.xreftext(xrefstyle)
807
+ }
808
+
809
+ /**
810
+ * Get the source location of this block.
811
+ * @returns {Cursor|undefined} the Cursor source location object, or undefined when sourcemap is disabled.
812
+ */
813
+ getSourceLocation() {
814
+ return this.sourceLocation ?? undefined
815
+ }
816
+
817
+ /**
818
+ * Get the list of substitutions enabled for this block.
819
+ * @returns {string[]}
820
+ */
821
+ getSubstitutions() {
822
+ return this.subs
823
+ }
824
+
825
+ /**
826
+ * Check whether the specified substitution is enabled for this block.
827
+ * @param {string} name
828
+ * @returns {boolean}
829
+ */
830
+ hasSubstitution(name) {
831
+ return this.hasSub(name)
832
+ }
833
+
834
+ /**
835
+ * Add the specified substitution to this block's substitutions list.
836
+ * @param {string} name
837
+ */
838
+ addSubstitution(name) {
839
+ if (!this.subs.includes(name)) this.subs.push(name)
840
+ }
841
+
842
+ /**
843
+ * Remove the specified substitution from this block's substitutions list.
844
+ * @param {string} name
845
+ */
846
+ removeSubstitution(name) {
847
+ this.removeSub(name)
848
+ }
849
+ }