@asciidoctor/core 4.0.1 → 4.0.3

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.
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const node_module = require('node:module');
4
+ const node_url = require('node:url');
4
5
  const node_fs = require('node:fs');
5
6
  const path = require('node:path');
6
7
 
7
8
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
- var version = "4.0.1";
9
+ var version = "4.0.3";
9
10
  const packageJson = {
10
11
  version: version};
11
12
 
@@ -3039,9 +3040,10 @@ class AbstractNode {
3039
3040
  * Construct a URI reference or data URI to the target image.
3040
3041
  *
3041
3042
  * If the target image is already a URI it is left untouched (unless data-uri
3042
- * conversion is requested). The image is resolved relative to the directory
3043
- * named by assetDirKey. When data-uri is enabled and the safe level permits,
3044
- * the image is embedded as a Base64 data URI.
3043
+ * conversion is requested). If the target image is a data URI, then it is
3044
+ * already an embedded image, so it is returned as-is. The image is resolved
3045
+ * relative to the directory named by assetDirKey. When data-uri is enabled and
3046
+ * the safe level permits, the image is embedded as a Base64 data URI.
3045
3047
  *
3046
3048
  * NOTE: When the document has both 'data-uri' and 'allow-uri-read' enabled
3047
3049
  * and the resolved image URL is a remote URI, this method returns a Promise
@@ -3052,6 +3054,8 @@ class AbstractNode {
3052
3054
  * @returns {Promise<string>} a Promise resolving to a String reference or data URI.
3053
3055
  */
3054
3056
  async imageUri(targetImage, assetDirKey = 'imagesdir') {
3057
+ // A data URI is already an embedded image, so use it as-is rather than reading or re-encoding it.
3058
+ if (targetImage.startsWith('data:')) return targetImage
3055
3059
  const doc = this.document;
3056
3060
  if (doc.safe < SafeMode.SECURE && doc.hasAttribute('data-uri')) {
3057
3061
  let imagesBase;
@@ -5459,7 +5463,18 @@ class SecurityError extends Error {
5459
5463
  function applyBackendTraits(instance) {
5460
5464
  instance._backendTraits = null;
5461
5465
 
5462
- instance.basebackend = function (value = null) {
5466
+ // Install a Ruby-style trait accessor method, but never clobber a flat string
5467
+ // property the converter already declared (convention #2). Overwriting e.g.
5468
+ // `converter.outfilesuffix = '.html'` with a method would silently turn the
5469
+ // author's string into a function; the backend traits stay reachable through
5470
+ // `_getBackendTraits()` instead.
5471
+ const defineTraitAccessor = (name, fn) => {
5472
+ const existing = Object.getOwnPropertyDescriptor(instance, name);
5473
+ if (existing && typeof existing.value !== 'function') return
5474
+ instance[name] = fn;
5475
+ };
5476
+
5477
+ defineTraitAccessor('basebackend', function (value = null) {
5463
5478
  if (value) {
5464
5479
  const traits = (this._backendTraits ??= {});
5465
5480
  traits.basebackend = value;
@@ -5473,19 +5488,19 @@ function applyBackendTraits(instance) {
5473
5488
  return value
5474
5489
  }
5475
5490
  return this._getBackendTraits().basebackend
5476
- };
5477
- instance.filetype = function (value = null) {
5491
+ });
5492
+ defineTraitAccessor('filetype', function (value = null) {
5478
5493
  if (value) return (this._getBackendTraits().filetype = value)
5479
5494
  return this._getBackendTraits().filetype
5480
- };
5481
- instance.htmlsyntax = function (value = null) {
5495
+ });
5496
+ defineTraitAccessor('htmlsyntax', function (value = null) {
5482
5497
  if (value) return (this._getBackendTraits().htmlsyntax = value)
5483
5498
  return this._getBackendTraits().htmlsyntax
5484
- };
5485
- instance.outfilesuffix = function (value = null) {
5499
+ });
5500
+ defineTraitAccessor('outfilesuffix', function (value = null) {
5486
5501
  if (value) return (this._getBackendTraits().outfilesuffix = value)
5487
5502
  return this._getBackendTraits().outfilesuffix
5488
- };
5503
+ });
5489
5504
  instance.supportsTemplates = function (value = true) {
5490
5505
  this._getBackendTraits().supportsTemplates = value;
5491
5506
  };
@@ -5569,7 +5584,9 @@ function normalizeConverter(converter, backend) {
5569
5584
  }
5570
5585
  }
5571
5586
 
5572
- // Apply the BackendTraits mixin so Document can call the standard accessor methods.
5587
+ // Apply the BackendTraits mixin so Document can read traits via
5588
+ // _getBackendTraits(). Flat string properties (convention #2) are preserved:
5589
+ // applyBackendTraits does not overwrite an existing same-named data property.
5573
5590
  applyBackendTraits(converter);
5574
5591
  if (traits) {
5575
5592
  converter._backendTraits = traits;
@@ -13506,6 +13523,30 @@ function _uniform(str, chr, len) {
13506
13523
  * @typedef {ProcessorDslInterface & { prefer(): void; prepend(): void }} DocumentProcessorDslInterface
13507
13524
  */
13508
13525
 
13526
+ /**
13527
+ * DSL interface for preprocessors.
13528
+ *
13529
+ * @typedef {Omit<DocumentProcessorDslInterface, 'process'> & {
13530
+ * process(fn: (this: PreprocessorDslInterface, document: Document, reader: Reader) => Reader | void): void;
13531
+ * }} PreprocessorDslInterface
13532
+ */
13533
+
13534
+ /**
13535
+ * DSL interface for tree processors.
13536
+ *
13537
+ * @typedef {Omit<DocumentProcessorDslInterface, 'process'> & {
13538
+ * process(fn: (this: TreeProcessorDslInterface, document: Document) => Document | void): void;
13539
+ * }} TreeProcessorDslInterface
13540
+ */
13541
+
13542
+ /**
13543
+ * DSL interface for postprocessors.
13544
+ *
13545
+ * @typedef {Omit<DocumentProcessorDslInterface, 'process'> & {
13546
+ * process(fn: (this: PostprocessorDslInterface, document: Document, output: string) => string): void;
13547
+ * }} PostprocessorDslInterface
13548
+ */
13549
+
13509
13550
  /**
13510
13551
  * DSL interface for syntax processors (BlockProcessor, BlockMacroProcessor, InlineMacroProcessor).
13511
13552
  *
@@ -13526,28 +13567,35 @@ function _uniform(str, chr, len) {
13526
13567
  /**
13527
13568
  * DSL interface for include processors.
13528
13569
  *
13529
- * @typedef {DocumentProcessorDslInterface & {
13570
+ * @typedef {Omit<DocumentProcessorDslInterface, 'process'> & {
13530
13571
  * handles(fn: (target: string) => boolean): void;
13531
13572
  * handles(fn: (doc: Document, target: string) => boolean): void;
13573
+ * process(fn: (this: IncludeProcessorDslInterface, document: Document, reader: Reader, target: string, attributes: Record<string, string>) => void): void;
13532
13574
  * }} IncludeProcessorDslInterface
13533
13575
  */
13534
13576
 
13535
13577
  /**
13536
13578
  * DSL interface for docinfo processors.
13537
13579
  *
13538
- * @typedef {DocumentProcessorDslInterface & {
13580
+ * @typedef {Omit<DocumentProcessorDslInterface, 'process'> & {
13539
13581
  * atLocation(value: string): void;
13582
+ * process(fn: (this: DocinfoProcessorDslInterface, document: Document) => string): void;
13540
13583
  * }} DocinfoProcessorDslInterface
13541
13584
  */
13542
13585
 
13543
13586
  /**
13544
13587
  * DSL interface for block processors.
13545
13588
  *
13546
- * @typedef {SyntaxProcessorDslInterface & {
13589
+ * The `process` callback is bound to the processor instance, so `this` inside it
13590
+ * (and inside the registration function) exposes the `createBlock` helpers.
13591
+ *
13592
+ * @typedef {Omit<SyntaxProcessorDslInterface, 'process'> & {
13547
13593
  * contexts(...value: (string | string[])[]): void;
13548
13594
  * onContexts(...value: (string | string[])[]): void;
13549
13595
  * onContext(...value: (string | string[])[]): void;
13550
13596
  * bindTo(...value: (string | string[])[]): void;
13597
+ * createBlock(parent: AbstractBlock, context: string, source?: string | string[] | null, attrs?: object, opts?: object): Block;
13598
+ * process(fn: (this: BlockProcessorDslInterface, parent: AbstractBlock, reader: Reader, attributes: Record<string, unknown>) => AbstractBlock | void): void;
13551
13599
  * }} BlockProcessorDslInterface
13552
13600
  */
13553
13601
 
@@ -13557,14 +13605,31 @@ function _uniform(str, chr, len) {
13557
13605
  * @typedef {SyntaxProcessorDslInterface} MacroProcessorDslInterface
13558
13606
  */
13559
13607
 
13608
+ /**
13609
+ * DSL interface for block macro processors.
13610
+ *
13611
+ * The `process` callback is bound to the processor instance, so `this` inside it
13612
+ * (and inside the registration function) exposes the `createBlock` helpers.
13613
+ *
13614
+ * @typedef {Omit<MacroProcessorDslInterface, 'process'> & {
13615
+ * createBlock(parent: AbstractBlock, context: string, source?: string | string[] | null, attrs?: object, opts?: object): Block;
13616
+ * process(fn: (this: BlockMacroProcessorDslInterface, parent: AbstractBlock, target: string, attributes: Record<string, unknown>) => AbstractBlock | void): void;
13617
+ * }} BlockMacroProcessorDslInterface
13618
+ */
13619
+
13560
13620
  /**
13561
13621
  * DSL interface for inline macro processors.
13562
13622
  *
13563
- * @typedef {MacroProcessorDslInterface & {
13623
+ * The `process` callback is bound to the processor instance, so `this` inside it
13624
+ * (and inside the registration function) exposes the `createInline` helper.
13625
+ *
13626
+ * @typedef {Omit<MacroProcessorDslInterface, 'process'> & {
13564
13627
  * format(value: string): void;
13565
13628
  * matchFormat(value: string): void;
13566
13629
  * usingFormat(value: string): void;
13567
13630
  * match(value: RegExp): void;
13631
+ * createInline(parent: AbstractBlock, context: string, text: string, opts?: object): Inline;
13632
+ * process(fn: (this: InlineMacroProcessorDslInterface, parent: AbstractBlock, target: string, attributes: Record<string, unknown>) => Inline | void): void;
13568
13633
  * }} InlineMacroProcessorDslInterface
13569
13634
  */
13570
13635
 
@@ -14285,7 +14350,15 @@ class IncludeProcessor extends Processor {
14285
14350
  }
14286
14351
 
14287
14352
  /**
14288
- * @param {Document} doc - The document being parsed.
14353
+ * Decide whether this include processor handles the given target.
14354
+ *
14355
+ * Override this method in a subclass. The override may accept either just the
14356
+ * target string (Ruby-style, arity 1) or both the document and the target
14357
+ * (arity 2) — an arity-1 override is adapted at registration time so the
14358
+ * parser can always invoke it as `handles(doc, target)`. The first parameter
14359
+ * is therefore typed `Document | string` so both override shapes type-check.
14360
+ *
14361
+ * @param {Document|string} doc - The document being parsed, or (for a Ruby-style arity-1 override) the include target.
14289
14362
  * @param {string} target - The target of the include directive.
14290
14363
  * @returns {boolean} true if this processor handles the given target.
14291
14364
  */
@@ -14653,6 +14726,14 @@ class Registry {
14653
14726
  * this.process(function (doc, reader) { ... })
14654
14727
  * })
14655
14728
  *
14729
+ * @overload
14730
+ * @param {typeof Preprocessor} processor - A Preprocessor subclass or instance.
14731
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14732
+ *
14733
+ * @overload
14734
+ * @param {(this: PreprocessorDslInterface) => void} fn - Registration function bound to the preprocessor DSL.
14735
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14736
+ *
14656
14737
  * @param {...*} args - Class constructor, instance, or block function.
14657
14738
  * @returns {ProcessorExtension} the Extension stored in the registry.
14658
14739
  */
@@ -14686,6 +14767,14 @@ class Registry {
14686
14767
  /**
14687
14768
  * Register a TreeProcessor with the extension registry.
14688
14769
  *
14770
+ * @overload
14771
+ * @param {typeof TreeProcessor} processor - A TreeProcessor subclass or instance.
14772
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14773
+ *
14774
+ * @overload
14775
+ * @param {(this: TreeProcessorDslInterface) => void} fn - Registration function bound to the tree processor DSL.
14776
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14777
+ *
14689
14778
  * @param {...*} args - Class constructor, instance, or block function.
14690
14779
  * @returns {ProcessorExtension} the Extension stored in the registry.
14691
14780
  */
@@ -14739,6 +14828,14 @@ class Registry {
14739
14828
  /**
14740
14829
  * Register a Postprocessor with the extension registry.
14741
14830
  *
14831
+ * @overload
14832
+ * @param {typeof Postprocessor} processor - A Postprocessor subclass or instance.
14833
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14834
+ *
14835
+ * @overload
14836
+ * @param {(this: PostprocessorDslInterface) => void} fn - Registration function bound to the postprocessor DSL.
14837
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14838
+ *
14742
14839
  * @param {...*} args - Class constructor, instance, or block function.
14743
14840
  * @returns {ProcessorExtension} the Extension stored in the registry.
14744
14841
  */
@@ -14772,6 +14869,14 @@ class Registry {
14772
14869
  /**
14773
14870
  * Register an IncludeProcessor with the extension registry.
14774
14871
  *
14872
+ * @overload
14873
+ * @param {typeof IncludeProcessor} processor - An IncludeProcessor subclass or instance.
14874
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14875
+ *
14876
+ * @overload
14877
+ * @param {(this: IncludeProcessorDslInterface) => void} fn - Registration function bound to the include processor DSL.
14878
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14879
+ *
14775
14880
  * @param {...*} args - Class constructor, instance, or block function.
14776
14881
  * @returns {ProcessorExtension} the Extension stored in the registry.
14777
14882
  */
@@ -14810,6 +14915,14 @@ class Registry {
14810
14915
  /**
14811
14916
  * Register a DocinfoProcessor with the extension registry.
14812
14917
  *
14918
+ * @overload
14919
+ * @param {typeof DocinfoProcessor} processor - A DocinfoProcessor subclass or instance.
14920
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14921
+ *
14922
+ * @overload
14923
+ * @param {(this: DocinfoProcessorDslInterface) => void} fn - Registration function bound to the docinfo processor DSL.
14924
+ * @returns {ProcessorExtension} the Extension stored in the registry.
14925
+ *
14813
14926
  * @param {...*} args - Class constructor, instance, or block function.
14814
14927
  * @returns {ProcessorExtension} the Extension stored in the registry.
14815
14928
  */
@@ -14877,6 +14990,20 @@ class Registry {
14877
14990
  * this.process(function (parent, reader, attrs) { ... })
14878
14991
  * })
14879
14992
  *
14993
+ * @overload
14994
+ * @param {typeof BlockProcessor} processor - A BlockProcessor subclass.
14995
+ * @param {string} [name] - Optional explicit block name.
14996
+ * @returns {ProcessorExtension} an Extension proxy object.
14997
+ *
14998
+ * @overload
14999
+ * @param {string} name - The block name.
15000
+ * @param {(this: BlockProcessorDslInterface) => void} fn - Registration function bound to the block DSL.
15001
+ * @returns {ProcessorExtension} an Extension proxy object.
15002
+ *
15003
+ * @overload
15004
+ * @param {(this: BlockProcessorDslInterface) => void} fn - Registration function bound to the block DSL.
15005
+ * @returns {ProcessorExtension} an Extension proxy object.
15006
+ *
14880
15007
  * @param {...*} args - Class constructor, instance, block function, or name + one of those.
14881
15008
  * @returns {ProcessorExtension} an Extension proxy object.
14882
15009
  */
@@ -14927,6 +15054,20 @@ class Registry {
14927
15054
  /**
14928
15055
  * Register a BlockMacroProcessor with the extension registry.
14929
15056
  *
15057
+ * @overload
15058
+ * @param {typeof BlockMacroProcessor} processor - A BlockMacroProcessor subclass.
15059
+ * @param {string} [name] - Optional explicit macro name.
15060
+ * @returns {ProcessorExtension} the Extension stored in the registry.
15061
+ *
15062
+ * @overload
15063
+ * @param {string} name - The macro name.
15064
+ * @param {(this: BlockMacroProcessorDslInterface) => void} fn - Registration function bound to the block macro DSL.
15065
+ * @returns {ProcessorExtension} the Extension stored in the registry.
15066
+ *
15067
+ * @overload
15068
+ * @param {(this: BlockMacroProcessorDslInterface) => void} fn - Registration function bound to the block macro DSL.
15069
+ * @returns {ProcessorExtension} the Extension stored in the registry.
15070
+ *
14930
15071
  * @param {...*} args - Class constructor, instance, or block function.
14931
15072
  * @returns {ProcessorExtension} the Extension stored in the registry.
14932
15073
  */
@@ -14982,6 +15123,20 @@ class Registry {
14982
15123
  /**
14983
15124
  * Register an InlineMacroProcessor with the extension registry.
14984
15125
  *
15126
+ * @overload
15127
+ * @param {typeof InlineMacroProcessor} processor - An InlineMacroProcessor subclass.
15128
+ * @param {string} [name] - Optional explicit macro name.
15129
+ * @returns {ProcessorExtension} the Extension stored in the registry.
15130
+ *
15131
+ * @overload
15132
+ * @param {string} name - The macro name.
15133
+ * @param {(this: InlineMacroProcessorDslInterface) => void} fn - Registration function bound to the inline macro DSL.
15134
+ * @returns {ProcessorExtension} the Extension stored in the registry.
15135
+ *
15136
+ * @overload
15137
+ * @param {(this: InlineMacroProcessorDslInterface) => void} fn - Registration function bound to the inline macro DSL.
15138
+ * @returns {ProcessorExtension} the Extension stored in the registry.
15139
+ *
14985
15140
  * @param {...*} args - Class constructor, instance, or block function.
14986
15141
  * @returns {ProcessorExtension} the Extension stored in the registry.
14987
15142
  */
@@ -16098,7 +16253,7 @@ class Document extends AbstractBlock {
16098
16253
 
16099
16254
  const attrOverrides = this._attributeOverrides;
16100
16255
  attrOverrides.asciidoctor = '';
16101
- attrOverrides['asciidoctor-version'] = '3.0.0.dev'; // matches Ruby VERSION
16256
+ attrOverrides['asciidoctor-version'] = packageJson.version;
16102
16257
 
16103
16258
  const safeModeName = SafeMode.nameForValue(this.safe);
16104
16259
  attrOverrides['safe-mode-name'] = safeModeName;
@@ -16290,16 +16445,16 @@ class Document extends AbstractBlock {
16290
16445
  // attribute (re)assignments are in scope; snapshot and restore the document
16291
16446
  // attributes so the downstream steps and conversion still start from the restored
16292
16447
  // (header) state, matching Ruby's restore_attributes-before-convert invariant.
16448
+ //
16449
+ // This runs in two passes because list item / table cell / dlist text may contain
16450
+ // natural cross-references (e.g. <<Some section title>>) that resolve against the
16451
+ // reftext→id map. Pass 1 substitutes titles and reftexts only, so every section
16452
+ // reftext is known; the map is then built; pass 2 substitutes the block content
16453
+ // text, where resolveId() can now match those natural references.
16293
16454
  const attributesSnapshot = { ...this.attributes };
16294
- await this._resolveAllTexts(this);
16295
- for (const key of Reflect.ownKeys(this.attributes))
16296
- delete this.attributes[key];
16297
- Object.assign(this.attributes, attributesSnapshot);
16298
- // Reset the footnote counter so that body-content footnotes (processed during conversion)
16299
- // start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
16300
- // numbered during parsing via apply_title_subs, then the counter restarts for body content.
16301
- delete this.attributes['footnote-number'];
16302
- delete this._counters['footnote-number'];
16455
+ // Pass 1: titles + reftexts (no block content text yet).
16456
+ await this._resolveAllTexts(this, false);
16457
+ this._restoreAttributeSnapshot(attributesSnapshot);
16303
16458
  // Pre-compute reftext for all registered inline anchor nodes.
16304
16459
  for (const ref of Object.values(this.catalog.refs)) {
16305
16460
  if (ref && typeof ref.precomputeReftext === 'function') {
@@ -16308,6 +16463,15 @@ class Document extends AbstractBlock {
16308
16463
  }
16309
16464
  // Build the reftext→id lookup map so that resolveId() is synchronous.
16310
16465
  await this._buildReftextsMap();
16466
+ // Pass 2: list item / table cell / dlist text, now that natural cross-references
16467
+ // can be resolved against the reftext→id map.
16468
+ await this._resolveAllTexts(this, true);
16469
+ this._restoreAttributeSnapshot(attributesSnapshot);
16470
+ // Reset the footnote counter so that body-content footnotes (processed during conversion)
16471
+ // start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
16472
+ // numbered during parsing via apply_title_subs, then the counter restarts for body content.
16473
+ delete this.attributes['footnote-number'];
16474
+ delete this._counters['footnote-number'];
16311
16475
 
16312
16476
  this._parsed = true;
16313
16477
  return this
@@ -17282,6 +17446,10 @@ class Document extends AbstractBlock {
17282
17446
  cell._innerDocument &&
17283
17447
  cell._innerContent == null
17284
17448
  ) {
17449
+ // Recurse into the inner document first so that AsciiDoc cells
17450
+ // of any nested table have their content computed before the
17451
+ // inner document (and the nested table) is rendered.
17452
+ await this._convertAsciiDocCells(cell._innerDocument);
17285
17453
  cell._innerContent = await cell._innerDocument.convert();
17286
17454
  }
17287
17455
  }
@@ -17362,12 +17530,33 @@ class Document extends AbstractBlock {
17362
17530
  : ['attributes']
17363
17531
  }
17364
17532
 
17533
+ /**
17534
+ * @private
17535
+ * Restore the document attributes to a previously captured snapshot, discarding any
17536
+ * body-level (re)assignments replayed while pre-computing text. Mirrors Ruby's
17537
+ * restore_attributes-before-convert invariant.
17538
+ * @param {Object} snapshot - The attributes snapshot to restore.
17539
+ */
17540
+ _restoreAttributeSnapshot(snapshot) {
17541
+ for (const key of Reflect.ownKeys(this.attributes))
17542
+ delete this.attributes[key];
17543
+ Object.assign(this.attributes, snapshot);
17544
+ }
17545
+
17365
17546
  /**
17366
17547
  * @private
17367
17548
  * Walk the block tree and pre-compute all async text values.
17368
17549
  * Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
17369
- */
17370
- async _resolveAllTexts(block) {
17550
+ *
17551
+ * Runs in two passes (see {@link parse}): with `resolveContent` false only titles and
17552
+ * reftexts are substituted (so the reftext→id map can be built); with `resolveContent`
17553
+ * true the list item / table cell / dlist text is substituted, resolving any natural
17554
+ * cross-references against the now-complete map. Title/reftext pre-computation is
17555
+ * idempotent (results are cached), so running it in both passes is a no-op the second time.
17556
+ * @param {AbstractBlock} block - The block to resolve.
17557
+ * @param {boolean} resolveContent - Whether to substitute list item / cell / dlist text.
17558
+ */
17559
+ async _resolveAllTexts(block, resolveContent) {
17371
17560
  // Replay this block's attribute entries (in document order, since the walk is
17372
17561
  // depth-first pre-order like conversion) so that body-level attribute assignments
17373
17562
  // and reassignments are in scope when the block's — and its descendants' and later
@@ -17393,12 +17582,12 @@ class Document extends AbstractBlock {
17393
17582
  // dlist.blocks is an array of [[term, ...], item_or_null] pairs.
17394
17583
  for (const [terms, item] of block.blocks ?? []) {
17395
17584
  for (const term of terms ?? []) {
17396
- await term.precomputeText?.();
17397
- await this._resolveAllTexts(term);
17585
+ if (resolveContent) await term.precomputeText?.();
17586
+ await this._resolveAllTexts(term, resolveContent);
17398
17587
  }
17399
17588
  if (item) {
17400
- await item.precomputeText?.();
17401
- await this._resolveAllTexts(item);
17589
+ if (resolveContent) await item.precomputeText?.();
17590
+ await this._resolveAllTexts(item, resolveContent);
17402
17591
  }
17403
17592
  }
17404
17593
  } else if (ctx === 'table') {
@@ -17408,14 +17597,14 @@ class Document extends AbstractBlock {
17408
17597
  ...(block.rows?.foot ?? []),
17409
17598
  ]) {
17410
17599
  for (const cell of row) {
17411
- await cell.precomputeText?.();
17600
+ if (resolveContent) await cell.precomputeText?.();
17412
17601
  await cell.precomputeReftext?.();
17413
17602
  }
17414
17603
  }
17415
17604
  } else {
17416
17605
  for (const child of block.blocks ?? []) {
17417
- await child.precomputeText?.();
17418
- await this._resolveAllTexts(child);
17606
+ if (resolveContent) await child.precomputeText?.();
17607
+ await this._resolveAllTexts(child, resolveContent);
17419
17608
  }
17420
17609
  }
17421
17610
  }
@@ -17707,14 +17896,19 @@ class Document extends AbstractBlock {
17707
17896
  let newBasebackend, newFiletype;
17708
17897
 
17709
17898
  if (converter && typeof converter._getBackendTraits === 'function') {
17710
- newBasebackend = converter.basebackend();
17711
- newFiletype = converter.filetype();
17712
- const htmlsyntax = converter.htmlsyntax();
17899
+ // Read the traits object directly rather than the same-named accessor
17900
+ // methods. A user converter that declares flat string properties
17901
+ // (`converter.outfilesuffix = '.html'`, convention #2) keeps them intact,
17902
+ // so callers reading `converter.outfilesuffix` still see the string.
17903
+ const traits = converter._getBackendTraits();
17904
+ newBasebackend = traits.basebackend;
17905
+ newFiletype = traits.filetype;
17906
+ const htmlsyntax = traits.htmlsyntax;
17713
17907
  if (htmlsyntax) attrs.htmlsyntax = htmlsyntax;
17714
17908
  if (init) {
17715
- attrs.outfilesuffix ??= converter.outfilesuffix();
17909
+ attrs.outfilesuffix ??= traits.outfilesuffix;
17716
17910
  } else if (!this.isAttributeLocked('outfilesuffix')) {
17717
- attrs.outfilesuffix = converter.outfilesuffix();
17911
+ attrs.outfilesuffix = traits.outfilesuffix;
17718
17912
  }
17719
17913
  } else if (converter) {
17720
17914
  const traits = deriveBackendTraits(newBackend);
@@ -21025,7 +21219,7 @@ class Html5Converter extends ConverterBase {
21025
21219
  let reproducible;
21026
21220
  if (!(reproducible = node.hasAttribute('reproducible'))) {
21027
21221
  result.push(
21028
- `<meta name="generator" content="Asciidoctor ${node.getAttribute('asciidoctor-version')}"${slash}>`
21222
+ `<meta name="generator" content="Asciidoctor.js ${node.getAttribute('asciidoctor-version')}"${slash}>`
21029
21223
  );
21030
21224
  }
21031
21225
  if (node.hasAttribute('app-name')) {
@@ -21742,7 +21936,9 @@ ${await node.content()}
21742
21936
  const slash = this._voidSlash;
21743
21937
  let img, src;
21744
21938
  if (
21745
- (node.hasAttribute('format', 'svg') || target.includes('.svg')) &&
21939
+ (node.hasAttribute('format', 'svg') ||
21940
+ target.includes('.svg') ||
21941
+ target.startsWith('data:image/svg+xml')) &&
21746
21942
  node.document.safe < SafeMode.SECURE
21747
21943
  ) {
21748
21944
  if (node.hasOption('inline')) {
@@ -22583,7 +22779,9 @@ Your browser does not support the video tag.
22583
22779
  if (node.hasAttribute('title'))
22584
22780
  attrs += ` title="${node.getAttribute('title')}"`;
22585
22781
  if (
22586
- (node.hasAttribute('format', 'svg') || target.includes('.svg')) &&
22782
+ (node.hasAttribute('format', 'svg') ||
22783
+ target.includes('.svg') ||
22784
+ target.startsWith('data:image/svg+xml')) &&
22587
22785
  node.document.safe < SafeMode.SECURE
22588
22786
  ) {
22589
22787
  if (node.hasOption('inline')) {
@@ -22676,12 +22874,17 @@ Your browser does not support the video tag.
22676
22874
 
22677
22875
  // NOTE expose readSvgContents for Bespoke converter
22678
22876
  async readSvgContents(node, target) {
22679
- let svg = await node.readContents(target, {
22680
- start: node.document.getAttribute('imagesdir'),
22681
- normalize: true,
22682
- label: 'SVG',
22683
- warnIfEmpty: true,
22684
- });
22877
+ // A data-URI carries the SVG in the target itself (e.g. an embedded diagram
22878
+ // produced with `:data-uri:`), so decode it directly rather than trying to
22879
+ // read it as a file or remote URI.
22880
+ let svg = target.startsWith('data:')
22881
+ ? this._decodeDataUri(target)
22882
+ : await node.readContents(target, {
22883
+ start: node.document.getAttribute('imagesdir'),
22884
+ normalize: true,
22885
+ label: 'SVG',
22886
+ warnIfEmpty: true,
22887
+ });
22685
22888
  if (!svg) return null
22686
22889
  if (!svg.startsWith('<svg')) svg = svg.replace(SvgPreambleRx, '');
22687
22890
  // Fix incomplete SVG start tag (missing closing >) by inserting > before the first child element.
@@ -22713,6 +22916,27 @@ Your browser does not support the video tag.
22713
22916
 
22714
22917
  // ── Private helpers ─────────────────────────────────────────────────────────
22715
22918
 
22919
+ /**
22920
+ * Decode an inline `data:` URI to its text contents (e.g. an SVG document) so
22921
+ * an image whose target is a data-URI can be embedded inline. Supports both
22922
+ * Base64 (`;base64,`) and percent-encoded payloads. Returns null when the
22923
+ * payload is missing.
22924
+ *
22925
+ * @internal
22926
+ * @private
22927
+ */
22928
+ _decodeDataUri(target) {
22929
+ const comma = target.indexOf(',');
22930
+ if (comma === -1) return null
22931
+ const meta = target.slice('data:'.length, comma);
22932
+ const data = target.slice(comma + 1);
22933
+ if (/;base64\b/i.test(meta)) {
22934
+ const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
22935
+ return new TextDecoder('utf-8').decode(bytes)
22936
+ }
22937
+ return decodeURIComponent(data)
22938
+ }
22939
+
22716
22940
  /**
22717
22941
  * @internal
22718
22942
  * @private
@@ -22955,8 +23179,7 @@ const composite = /*#__PURE__*/Object.freeze({
22955
23179
  // - Ruby File.directory?/File.file? → fsp.stat().isDirectory()/isFile() (async).
22956
23180
  // - Ruby File.basename / File.expand_path → node:path basename / resolve.
22957
23181
  // - PathResolver.system_path → pathResolver.systemPath().
22958
- // - template.render(node, opts) → template.render({node, opts, helpers}).
22959
- // - Helpers loaded from helpers.js/helpers.cjs; can export configure(enginesContext).
23182
+ // - template.render(node, opts) → template.render({node, opts, helpers}).// - Helpers loaded from helpers.js/helpers.cjs/helpers.mjs; can export configure(enginesContext).
22960
23183
  // - Custom engines registered via TemplateConverter.TemplateEngine.register(ext, adapter).
22961
23184
  // - Thread safety / Mutex → not needed (single-threaded JS).
22962
23185
  // - load_eruby / eRuby support → not applicable in JS environment.
@@ -23230,7 +23453,11 @@ class TemplateConverter extends ConverterBase {
23230
23453
  if (!(await _isFile(file))) continue
23231
23454
 
23232
23455
  // Collect helpers separately; process after all templates.
23233
- if (basename === 'helpers.js' || basename === 'helpers.cjs') {
23456
+ if (
23457
+ basename === 'helpers.js' ||
23458
+ basename === 'helpers.cjs' ||
23459
+ basename === 'helpers.mjs'
23460
+ ) {
23234
23461
  helpersFile = file;
23235
23462
  continue
23236
23463
  }
@@ -23298,8 +23525,8 @@ class TemplateConverter extends ConverterBase {
23298
23525
  const pugOpts = { ...(this.engineOptions.pug ?? {}), filename: file };
23299
23526
  const renderFn = pug.compileFile(file, pugOpts);
23300
23527
  template = { render: renderFn, file };
23301
- } else if (ext === 'js' || ext === 'cjs') {
23302
- const renderFn = _require(file);
23528
+ } else if (ext === 'js' || ext === 'cjs' || ext === 'mjs') {
23529
+ const renderFn = await _loadModule(file, ext);
23303
23530
  template = { render: renderFn, file };
23304
23531
  } else {
23305
23532
  // Fall back to custom TemplateEngine registry.
@@ -23314,16 +23541,20 @@ class TemplateConverter extends ConverterBase {
23314
23541
  result[name] = template;
23315
23542
  }
23316
23543
 
23317
- // Load helpers if found (or if a helpers.js exists at the top of the dir).
23318
- const fallbackHelpers = path.join(templateDir, 'helpers.js');
23319
- const fallbackHelpersCjs = path.join(templateDir, 'helpers.cjs');
23320
- if (!helpersFile && (await _isFile(fallbackHelpers)))
23321
- helpersFile = fallbackHelpers;
23322
- if (!helpersFile && (await _isFile(fallbackHelpersCjs)))
23323
- helpersFile = fallbackHelpersCjs;
23544
+ // Load helpers if found (or if a helpers file exists at the top of the dir).
23545
+ if (!helpersFile) {
23546
+ for (const ext of ['js', 'cjs', 'mjs']) {
23547
+ const fallback = path.join(templateDir, `helpers.${ext}`);
23548
+ if (await _isFile(fallback)) {
23549
+ helpersFile = fallback;
23550
+ break
23551
+ }
23552
+ }
23553
+ }
23324
23554
 
23325
23555
  if (helpersFile) {
23326
- const ctx = _require(helpersFile);
23556
+ const helpersExt = helpersFile.slice(helpersFile.lastIndexOf('.') + 1);
23557
+ const ctx = await _loadModule(helpersFile, helpersExt);
23327
23558
  if (typeof ctx.configure === 'function') ctx.configure(enginesCtx);
23328
23559
  result['helpers.js'] = { file: helpersFile, ctx };
23329
23560
  }
@@ -23351,6 +23582,32 @@ class TemplateConverter extends ConverterBase {
23351
23582
 
23352
23583
  // ── Helpers ───────────────────────────────────────────────────────────────────
23353
23584
 
23585
+ /**
23586
+ * Load a JavaScript module (template render function or helpers object) by
23587
+ * file extension.
23588
+ *
23589
+ * - `.cjs` files are always CommonJS: require() returns `module.exports`.
23590
+ * - `.js` and `.mjs` files are loaded with a dynamic `import()`, which Node
23591
+ * resolves as either ES modules or CommonJS. The export is taken from the
23592
+ * module's default export (`export default …` in ESM, `module.exports = …`
23593
+ * in CommonJS via interop); when there is no default export the module
23594
+ * namespace is returned instead, so ESM helpers exposing named exports
23595
+ * (`export function configure() {}`) work too.
23596
+ * @param {string} file - absolute path to the module file
23597
+ * @param {string} ext - file extension without the leading dot ('js' | 'cjs' | 'mjs')
23598
+ * @returns {Promise<*>} the module's default export, or its namespace
23599
+ * @internal
23600
+ * @private
23601
+ */
23602
+ async function _loadModule(file, ext) {
23603
+ if (ext === 'cjs') {
23604
+ const mod = _require(file);
23605
+ return mod?.default ? mod.default : mod
23606
+ }
23607
+ const mod = await import(node_url.pathToFileURL(file).href);
23608
+ return mod.default ?? mod
23609
+ }
23610
+
23354
23611
  /**
23355
23612
  * @internal
23356
23613
  * @private
@@ -24539,7 +24796,7 @@ class ManPageConverter extends ConverterBase {
24539
24796
  `'\\" t
24540
24797
  .\\" Title: ${mantitle}
24541
24798
  .\\" Author: ${node.hasAttribute('authors') ? node.getAttribute('authors') : '[see the "AUTHOR(S)" section]'}
24542
- .\\" Generator: Asciidoctor ${node.getAttribute('asciidoctor-version')}`,
24799
+ .\\" Generator: Asciidoctor.js ${node.getAttribute('asciidoctor-version')}`,
24543
24800
  ];
24544
24801
 
24545
24802
  if (docdate) result.push(`.\\" Date: ${docdate}`);