@asciidoctor/core 4.0.0-alpha.5 → 4.0.0

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,4 +1,4 @@
1
- var version = "4.0.0-alpha.5";
1
+ var version = "4.0.0";
2
2
  const packageJson = {
3
3
  version: version};
4
4
 
@@ -4079,7 +4079,7 @@ class AbstractBlock extends AbstractNode {
4079
4079
  if (anyContext || contextSelector !== 'section') {
4080
4080
  // optimisation
4081
4081
  // NOTE dlist items can be null
4082
- for (const b of this.blocks.flat()) {
4082
+ for (const b of this.blocks.flat(Infinity)) {
4083
4083
  if (b) b.#findByInternal(selector, result, filter);
4084
4084
  }
4085
4085
  }
@@ -8921,6 +8921,25 @@ class Cell extends AbstractBlock {
8921
8921
  if (cell._innerDocSetup) {
8922
8922
  const { lines, parentDoc, parentDoctitle, options } = cell._innerDocSetup;
8923
8923
  cell._innerDocSetup = null;
8924
+ // If the first line may be a preprocessor directive (include, ifdef…), expand it using a
8925
+ // temporary PreprocessorReader — matching the Ruby behaviour in table.rb.
8926
+ if (lines.length > 0 && lines[0].includes('::')) {
8927
+ const firstLine = lines[0];
8928
+ const tmpReader = new PreprocessorReader(
8929
+ parentDoc,
8930
+ [firstLine],
8931
+ options.cursor
8932
+ );
8933
+ const preprocessedLines = await tmpReader.readLines();
8934
+ if (
8935
+ !(
8936
+ preprocessedLines.length === 1 && preprocessedLines[0] === firstLine
8937
+ )
8938
+ ) {
8939
+ lines.shift();
8940
+ if (preprocessedLines.length > 0) lines.unshift(...preprocessedLines);
8941
+ }
8942
+ }
8924
8943
  const innerDoc = await parentDoc.constructor.create(lines, options);
8925
8944
  if (parentDoctitle) parentDoc.attributes.doctitle = parentDoctitle;
8926
8945
  cell._innerDocument = innerDoc;
@@ -11012,8 +11031,8 @@ class Parser {
11012
11031
  const admStyle = attributes.style ?? blockContext;
11013
11032
  attributes.name = admStyle.toLowerCase();
11014
11033
  attributes.textlabel =
11015
- (attributes.caption && delete attributes.caption) ??
11016
- docAttrs[`${attributes.name}-caption`];
11034
+ attributes.caption ?? docAttrs[`${attributes.name}-caption`];
11035
+ delete attributes.caption;
11017
11036
  block = await Parser.buildBlock(
11018
11037
  blockContext,
11019
11038
  'compound',
@@ -13447,6 +13466,7 @@ function _uniform(str, chr, len) {
13447
13466
  * DSL interface for include processors.
13448
13467
  *
13449
13468
  * @typedef {DocumentProcessorDslInterface & {
13469
+ * handles(fn: (target: string) => boolean): void;
13450
13470
  * handles(fn: (doc: Document, target: string) => boolean): void;
13451
13471
  * }} IncludeProcessorDslInterface
13452
13472
  */
@@ -13497,7 +13517,7 @@ function _uniform(str, chr, len) {
13497
13517
  * - Called with a single Function argument → stores it as the process block.
13498
13518
  * - Called with non-Function arguments → invokes the stored process block.
13499
13519
  *
13500
- * The this context inside a stored process function is bound to the processor
13520
+ * The `this` context inside a stored process function is bound to the processor
13501
13521
  * instance at definition time.
13502
13522
  */
13503
13523
  const ProcessorDsl = {
@@ -13671,6 +13691,35 @@ const SyntaxProcessorDsl = {
13671
13691
  const IncludeProcessorDsl = {
13672
13692
  ...DocumentProcessorDsl,
13673
13693
 
13694
+ /**
13695
+ * @overload
13696
+ * @param {(target: string) => boolean} fn - Predicate that receives only the include target.
13697
+ * @returns {void}
13698
+ */
13699
+ /**
13700
+ * @overload
13701
+ * @param {(doc: Document, target: string) => boolean} fn - Predicate that receives the document and the include target.
13702
+ * @returns {void}
13703
+ */
13704
+ /**
13705
+ * @overload
13706
+ * @param {Document} doc - The document being parsed.
13707
+ * @param {string} target - The include target.
13708
+ * @returns {boolean}
13709
+ */
13710
+ /**
13711
+ * Register a function that decides whether this include processor handles a given target,
13712
+ * or invoke the registered handler.
13713
+ *
13714
+ * **Setter form** — register a predicate. The callback may accept either just the target
13715
+ * string (arity 1) or both the document and the target string (arity 2).
13716
+ *
13717
+ * **Invoker form** — call the registered predicate with `(doc, target)` and return its
13718
+ * result, or return `true` if no predicate has been registered.
13719
+ *
13720
+ * @param {...*} args
13721
+ * @returns {boolean | void}
13722
+ */
13674
13723
  handles(...args) {
13675
13724
  if (args.length === 1 && typeof args[0] === 'function') {
13676
13725
  const fn = args[0];
@@ -14438,6 +14487,8 @@ class ProcessorExtension extends Extension {
14438
14487
  super(kind, instance, instance.config);
14439
14488
  this.processMethod =
14440
14489
  processMethod || ((...args) => instance.process(...args));
14490
+ /** @internal */
14491
+ this._direct = false;
14441
14492
  }
14442
14493
  }
14443
14494
 
@@ -14465,10 +14516,24 @@ const SYNTAX_PROCESSOR_CLASSES = {
14465
14516
  * Registry holds the extensions which have been registered and activated, has
14466
14517
  * methods for registering or defining a processor and looks up extensions
14467
14518
  * stored in the registry during parsing.
14519
+ *
14520
+ * A registry can be reused across multiple conversions. Extensions registered
14521
+ * via a group block (passed to {@link Extensions.create} or
14522
+ * {@link Extensions.register}) are re-executed on every activation. Extensions
14523
+ * registered directly on the registry instance (e.g. `registry.preprocessor(fn)`)
14524
+ * are preserved across activations.
14525
+ *
14526
+ * @example
14527
+ * const registry = Extensions.create('my-ext', function () {
14528
+ * this.preprocessor(function () { ... })
14529
+ * })
14530
+ * // registry can be passed to multiple conversions safely
14468
14531
  */
14469
14532
  class Registry {
14470
14533
  constructor(groups = {}) {
14471
14534
  this.groups = groups;
14535
+ /** @internal */
14536
+ this._activating = false;
14472
14537
  this._reset();
14473
14538
  }
14474
14539
 
@@ -14485,18 +14550,26 @@ class Registry {
14485
14550
  ...Object.values(Extensions.groups()),
14486
14551
  ...Object.values(this.groups),
14487
14552
  ];
14488
- for (const group of extGroups) {
14489
- if (typeof group === 'function') {
14490
- // Check if it is a class (constructor) with an activate prototype method.
14491
- if (group.prototype && typeof group.prototype.activate === 'function') {
14492
- new group().activate(this);
14493
- } else {
14494
- // Plain function — call in the context of this registry (like instance_exec).
14495
- group.length === 0 ? group.call(this) : group(this);
14553
+ this._activating = true;
14554
+ try {
14555
+ for (const group of extGroups) {
14556
+ if (typeof group === 'function') {
14557
+ // Check if it is a class (constructor) with an activate prototype method.
14558
+ if (
14559
+ group.prototype &&
14560
+ typeof group.prototype.activate === 'function'
14561
+ ) {
14562
+ new group().activate(this);
14563
+ } else {
14564
+ // Plain function — call in the context of this registry (like instance_exec).
14565
+ group.length === 0 ? group.call(this) : group(this);
14566
+ }
14567
+ } else if (group && typeof group.activate === 'function') {
14568
+ group.activate(this);
14496
14569
  }
14497
- } else if (group && typeof group.activate === 'function') {
14498
- group.activate(this);
14499
14570
  }
14571
+ } finally {
14572
+ this._activating = false;
14500
14573
  }
14501
14574
  return this
14502
14575
  }
@@ -15072,6 +15145,7 @@ class Registry {
15072
15145
  }
15073
15146
 
15074
15147
  const extension = new ProcessorExtension(kind, processorInstance);
15148
+ extension._direct = !this._activating;
15075
15149
  extension.config.position === '>>'
15076
15150
  ? store.unshift(extension)
15077
15151
  : store.push(extension);
@@ -15143,27 +15217,44 @@ class Registry {
15143
15217
  }
15144
15218
 
15145
15219
  store[name] = new ProcessorExtension(kind, processorInstance);
15220
+ store[name]._direct = !this._activating;
15146
15221
  return store[name]
15147
15222
  }
15148
15223
 
15149
15224
  /** @internal */
15150
15225
  _reset() {
15226
+ // Keep extensions registered directly (outside a group); only clear group-registered ones.
15227
+ // Extensions tagged with _direct=true survive across activations.
15228
+ const keepArr = (arr) => {
15229
+ const kept = arr?.filter((e) => e._direct) ?? [];
15230
+ return kept.length ? kept : null
15231
+ };
15232
+ const keepMap = (map) => {
15233
+ if (!map) return null
15234
+ const kept = Object.create(null);
15235
+ for (const [k, v] of Object.entries(map)) if (v._direct) kept[k] = v;
15236
+ return Object.keys(kept).length ? kept : null
15237
+ };
15151
15238
  /** @internal */
15152
- this._preprocessor_extensions = null;
15239
+ this._preprocessor_extensions = keepArr(this._preprocessor_extensions);
15153
15240
  /** @internal */
15154
- this._tree_processor_extensions = null;
15241
+ this._tree_processor_extensions = keepArr(this._tree_processor_extensions);
15155
15242
  /** @internal */
15156
- this._postprocessor_extensions = null;
15243
+ this._postprocessor_extensions = keepArr(this._postprocessor_extensions);
15157
15244
  /** @internal */
15158
- this._include_processor_extensions = null;
15245
+ this._include_processor_extensions = keepArr(
15246
+ this._include_processor_extensions
15247
+ );
15159
15248
  /** @internal */
15160
- this._docinfo_processor_extensions = null;
15249
+ this._docinfo_processor_extensions = keepArr(
15250
+ this._docinfo_processor_extensions
15251
+ );
15161
15252
  /** @internal */
15162
- this._block_extensions = null;
15253
+ this._block_extensions = keepMap(this._block_extensions);
15163
15254
  /** @internal */
15164
- this._block_macro_extensions = null;
15255
+ this._block_macro_extensions = keepMap(this._block_macro_extensions);
15165
15256
  /** @internal */
15166
- this._inline_macro_extensions = null;
15257
+ this._inline_macro_extensions = keepMap(this._inline_macro_extensions);
15167
15258
  this.document = null;
15168
15259
  }
15169
15260
 
@@ -15246,6 +15337,13 @@ const Extensions = {
15246
15337
  /**
15247
15338
  * Create a new Registry, optionally pre-populated with a named block.
15248
15339
  *
15340
+ * When a `block` is provided it is stored as a group and re-executed on every
15341
+ * activation, making the registry safe to reuse across multiple conversions.
15342
+ * Without a `block`, any extensions registered directly on the returned registry
15343
+ * (e.g. `registry.preprocessor(fn)`) are stored in transient state that is
15344
+ * cleared on every activation — those registrations will be lost from the second
15345
+ * conversion onwards. Prefer the block form when the registry may be reused.
15346
+ *
15249
15347
  * @param {string|null} [name=null] - Optional name for the group; auto-generated if omitted.
15250
15348
  * @param {Function|null} [block=null] - Optional function to register as the group.
15251
15349
  * @returns {Registry}
@@ -16022,8 +16120,8 @@ class Document extends AbstractBlock {
16022
16120
  if ((this.doctype = attrs.doctype = parentDoctype) !== DEFAULT_DOCTYPE) {
16023
16121
  this._updateDoctypeAttributes(DEFAULT_DOCTYPE);
16024
16122
  }
16025
- // Set up reader only parsing is deferred to Document.create() / doc.parse().
16026
- this.reader = new PreprocessorReader(this, data, options.cursor);
16123
+ // Nested documents use a plain Reader (no include/conditional processing), matching Ruby behaviour.
16124
+ this.reader = new Reader(data, options.cursor, { document: this });
16027
16125
  if (this.sourcemap) this.sourceLocation = this.reader.cursor;
16028
16126
  } else {
16029
16127
  this.backend = null;
@@ -5,7 +5,7 @@ const node_fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
 
7
7
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
- var version = "4.0.0-alpha.5";
8
+ var version = "4.0.0";
9
9
  const packageJson = {
10
10
  version: version};
11
11
 
@@ -4086,7 +4086,7 @@ class AbstractBlock extends AbstractNode {
4086
4086
  if (anyContext || contextSelector !== 'section') {
4087
4087
  // optimisation
4088
4088
  // NOTE dlist items can be null
4089
- for (const b of this.blocks.flat()) {
4089
+ for (const b of this.blocks.flat(Infinity)) {
4090
4090
  if (b) b.#findByInternal(selector, result, filter);
4091
4091
  }
4092
4092
  }
@@ -8928,6 +8928,25 @@ class Cell extends AbstractBlock {
8928
8928
  if (cell._innerDocSetup) {
8929
8929
  const { lines, parentDoc, parentDoctitle, options } = cell._innerDocSetup;
8930
8930
  cell._innerDocSetup = null;
8931
+ // If the first line may be a preprocessor directive (include, ifdef…), expand it using a
8932
+ // temporary PreprocessorReader — matching the Ruby behaviour in table.rb.
8933
+ if (lines.length > 0 && lines[0].includes('::')) {
8934
+ const firstLine = lines[0];
8935
+ const tmpReader = new PreprocessorReader(
8936
+ parentDoc,
8937
+ [firstLine],
8938
+ options.cursor
8939
+ );
8940
+ const preprocessedLines = await tmpReader.readLines();
8941
+ if (
8942
+ !(
8943
+ preprocessedLines.length === 1 && preprocessedLines[0] === firstLine
8944
+ )
8945
+ ) {
8946
+ lines.shift();
8947
+ if (preprocessedLines.length > 0) lines.unshift(...preprocessedLines);
8948
+ }
8949
+ }
8931
8950
  const innerDoc = await parentDoc.constructor.create(lines, options);
8932
8951
  if (parentDoctitle) parentDoc.attributes.doctitle = parentDoctitle;
8933
8952
  cell._innerDocument = innerDoc;
@@ -11019,8 +11038,8 @@ class Parser {
11019
11038
  const admStyle = attributes.style ?? blockContext;
11020
11039
  attributes.name = admStyle.toLowerCase();
11021
11040
  attributes.textlabel =
11022
- (attributes.caption && delete attributes.caption) ??
11023
- docAttrs[`${attributes.name}-caption`];
11041
+ attributes.caption ?? docAttrs[`${attributes.name}-caption`];
11042
+ delete attributes.caption;
11024
11043
  block = await Parser.buildBlock(
11025
11044
  blockContext,
11026
11045
  'compound',
@@ -13454,6 +13473,7 @@ function _uniform(str, chr, len) {
13454
13473
  * DSL interface for include processors.
13455
13474
  *
13456
13475
  * @typedef {DocumentProcessorDslInterface & {
13476
+ * handles(fn: (target: string) => boolean): void;
13457
13477
  * handles(fn: (doc: Document, target: string) => boolean): void;
13458
13478
  * }} IncludeProcessorDslInterface
13459
13479
  */
@@ -13504,7 +13524,7 @@ function _uniform(str, chr, len) {
13504
13524
  * - Called with a single Function argument → stores it as the process block.
13505
13525
  * - Called with non-Function arguments → invokes the stored process block.
13506
13526
  *
13507
- * The this context inside a stored process function is bound to the processor
13527
+ * The `this` context inside a stored process function is bound to the processor
13508
13528
  * instance at definition time.
13509
13529
  */
13510
13530
  const ProcessorDsl = {
@@ -13678,6 +13698,35 @@ const SyntaxProcessorDsl = {
13678
13698
  const IncludeProcessorDsl = {
13679
13699
  ...DocumentProcessorDsl,
13680
13700
 
13701
+ /**
13702
+ * @overload
13703
+ * @param {(target: string) => boolean} fn - Predicate that receives only the include target.
13704
+ * @returns {void}
13705
+ */
13706
+ /**
13707
+ * @overload
13708
+ * @param {(doc: Document, target: string) => boolean} fn - Predicate that receives the document and the include target.
13709
+ * @returns {void}
13710
+ */
13711
+ /**
13712
+ * @overload
13713
+ * @param {Document} doc - The document being parsed.
13714
+ * @param {string} target - The include target.
13715
+ * @returns {boolean}
13716
+ */
13717
+ /**
13718
+ * Register a function that decides whether this include processor handles a given target,
13719
+ * or invoke the registered handler.
13720
+ *
13721
+ * **Setter form** — register a predicate. The callback may accept either just the target
13722
+ * string (arity 1) or both the document and the target string (arity 2).
13723
+ *
13724
+ * **Invoker form** — call the registered predicate with `(doc, target)` and return its
13725
+ * result, or return `true` if no predicate has been registered.
13726
+ *
13727
+ * @param {...*} args
13728
+ * @returns {boolean | void}
13729
+ */
13681
13730
  handles(...args) {
13682
13731
  if (args.length === 1 && typeof args[0] === 'function') {
13683
13732
  const fn = args[0];
@@ -14445,6 +14494,8 @@ class ProcessorExtension extends Extension {
14445
14494
  super(kind, instance, instance.config);
14446
14495
  this.processMethod =
14447
14496
  processMethod || ((...args) => instance.process(...args));
14497
+ /** @internal */
14498
+ this._direct = false;
14448
14499
  }
14449
14500
  }
14450
14501
 
@@ -14472,10 +14523,24 @@ const SYNTAX_PROCESSOR_CLASSES = {
14472
14523
  * Registry holds the extensions which have been registered and activated, has
14473
14524
  * methods for registering or defining a processor and looks up extensions
14474
14525
  * stored in the registry during parsing.
14526
+ *
14527
+ * A registry can be reused across multiple conversions. Extensions registered
14528
+ * via a group block (passed to {@link Extensions.create} or
14529
+ * {@link Extensions.register}) are re-executed on every activation. Extensions
14530
+ * registered directly on the registry instance (e.g. `registry.preprocessor(fn)`)
14531
+ * are preserved across activations.
14532
+ *
14533
+ * @example
14534
+ * const registry = Extensions.create('my-ext', function () {
14535
+ * this.preprocessor(function () { ... })
14536
+ * })
14537
+ * // registry can be passed to multiple conversions safely
14475
14538
  */
14476
14539
  class Registry {
14477
14540
  constructor(groups = {}) {
14478
14541
  this.groups = groups;
14542
+ /** @internal */
14543
+ this._activating = false;
14479
14544
  this._reset();
14480
14545
  }
14481
14546
 
@@ -14492,18 +14557,26 @@ class Registry {
14492
14557
  ...Object.values(Extensions.groups()),
14493
14558
  ...Object.values(this.groups),
14494
14559
  ];
14495
- for (const group of extGroups) {
14496
- if (typeof group === 'function') {
14497
- // Check if it is a class (constructor) with an activate prototype method.
14498
- if (group.prototype && typeof group.prototype.activate === 'function') {
14499
- new group().activate(this);
14500
- } else {
14501
- // Plain function — call in the context of this registry (like instance_exec).
14502
- group.length === 0 ? group.call(this) : group(this);
14560
+ this._activating = true;
14561
+ try {
14562
+ for (const group of extGroups) {
14563
+ if (typeof group === 'function') {
14564
+ // Check if it is a class (constructor) with an activate prototype method.
14565
+ if (
14566
+ group.prototype &&
14567
+ typeof group.prototype.activate === 'function'
14568
+ ) {
14569
+ new group().activate(this);
14570
+ } else {
14571
+ // Plain function — call in the context of this registry (like instance_exec).
14572
+ group.length === 0 ? group.call(this) : group(this);
14573
+ }
14574
+ } else if (group && typeof group.activate === 'function') {
14575
+ group.activate(this);
14503
14576
  }
14504
- } else if (group && typeof group.activate === 'function') {
14505
- group.activate(this);
14506
14577
  }
14578
+ } finally {
14579
+ this._activating = false;
14507
14580
  }
14508
14581
  return this
14509
14582
  }
@@ -15079,6 +15152,7 @@ class Registry {
15079
15152
  }
15080
15153
 
15081
15154
  const extension = new ProcessorExtension(kind, processorInstance);
15155
+ extension._direct = !this._activating;
15082
15156
  extension.config.position === '>>'
15083
15157
  ? store.unshift(extension)
15084
15158
  : store.push(extension);
@@ -15150,27 +15224,44 @@ class Registry {
15150
15224
  }
15151
15225
 
15152
15226
  store[name] = new ProcessorExtension(kind, processorInstance);
15227
+ store[name]._direct = !this._activating;
15153
15228
  return store[name]
15154
15229
  }
15155
15230
 
15156
15231
  /** @internal */
15157
15232
  _reset() {
15233
+ // Keep extensions registered directly (outside a group); only clear group-registered ones.
15234
+ // Extensions tagged with _direct=true survive across activations.
15235
+ const keepArr = (arr) => {
15236
+ const kept = arr?.filter((e) => e._direct) ?? [];
15237
+ return kept.length ? kept : null
15238
+ };
15239
+ const keepMap = (map) => {
15240
+ if (!map) return null
15241
+ const kept = Object.create(null);
15242
+ for (const [k, v] of Object.entries(map)) if (v._direct) kept[k] = v;
15243
+ return Object.keys(kept).length ? kept : null
15244
+ };
15158
15245
  /** @internal */
15159
- this._preprocessor_extensions = null;
15246
+ this._preprocessor_extensions = keepArr(this._preprocessor_extensions);
15160
15247
  /** @internal */
15161
- this._tree_processor_extensions = null;
15248
+ this._tree_processor_extensions = keepArr(this._tree_processor_extensions);
15162
15249
  /** @internal */
15163
- this._postprocessor_extensions = null;
15250
+ this._postprocessor_extensions = keepArr(this._postprocessor_extensions);
15164
15251
  /** @internal */
15165
- this._include_processor_extensions = null;
15252
+ this._include_processor_extensions = keepArr(
15253
+ this._include_processor_extensions
15254
+ );
15166
15255
  /** @internal */
15167
- this._docinfo_processor_extensions = null;
15256
+ this._docinfo_processor_extensions = keepArr(
15257
+ this._docinfo_processor_extensions
15258
+ );
15168
15259
  /** @internal */
15169
- this._block_extensions = null;
15260
+ this._block_extensions = keepMap(this._block_extensions);
15170
15261
  /** @internal */
15171
- this._block_macro_extensions = null;
15262
+ this._block_macro_extensions = keepMap(this._block_macro_extensions);
15172
15263
  /** @internal */
15173
- this._inline_macro_extensions = null;
15264
+ this._inline_macro_extensions = keepMap(this._inline_macro_extensions);
15174
15265
  this.document = null;
15175
15266
  }
15176
15267
 
@@ -15253,6 +15344,13 @@ const Extensions = {
15253
15344
  /**
15254
15345
  * Create a new Registry, optionally pre-populated with a named block.
15255
15346
  *
15347
+ * When a `block` is provided it is stored as a group and re-executed on every
15348
+ * activation, making the registry safe to reuse across multiple conversions.
15349
+ * Without a `block`, any extensions registered directly on the returned registry
15350
+ * (e.g. `registry.preprocessor(fn)`) are stored in transient state that is
15351
+ * cleared on every activation — those registrations will be lost from the second
15352
+ * conversion onwards. Prefer the block form when the registry may be reused.
15353
+ *
15256
15354
  * @param {string|null} [name=null] - Optional name for the group; auto-generated if omitted.
15257
15355
  * @param {Function|null} [block=null] - Optional function to register as the group.
15258
15356
  * @returns {Registry}
@@ -16029,8 +16127,8 @@ class Document extends AbstractBlock {
16029
16127
  if ((this.doctype = attrs.doctype = parentDoctype) !== DEFAULT_DOCTYPE) {
16030
16128
  this._updateDoctypeAttributes(DEFAULT_DOCTYPE);
16031
16129
  }
16032
- // Set up reader only parsing is deferred to Document.create() / doc.parse().
16033
- this.reader = new PreprocessorReader(this, data, options.cursor);
16130
+ // Nested documents use a plain Reader (no include/conditional processing), matching Ruby behaviour.
16131
+ this.reader = new Reader(data, options.cursor, { document: this });
16034
16132
  if (this.sourcemap) this.sourceLocation = this.reader.cursor;
16035
16133
  } else {
16036
16134
  this.backend = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asciidoctor/core",
3
- "version": "4.0.0-alpha.5",
3
+ "version": "4.0.0",
4
4
  "description": "Asciidoctor.js: AsciiDoc in JavaScript powered by Asciidoctor",
5
5
  "type": "module",
6
6
  "main": "build/node/index.cjs",
@@ -66,16 +66,16 @@
66
66
  },
67
67
  "homepage": "https://github.com/asciidoctor/asciidoctor.js",
68
68
  "volta": {
69
- "node": "24.15.0"
69
+ "node": "24.16.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@biomejs/biome": "^2.4.13",
73
73
  "typedoc": "^0.28.4",
74
74
  "@rollup/plugin-json": "^6.1.0",
75
75
  "@types/node": "^25.6.0",
76
- "@vitest/browser": "^4.1.4",
77
- "@vitest/browser-playwright": "^4.1.4",
78
- "@vitest/ui": "^4.1.4",
76
+ "@vitest/browser": "^4.1.7",
77
+ "@vitest/browser-playwright": "^4.1.7",
78
+ "@vitest/ui": "^4.1.7",
79
79
  "@xmldom/xmldom": "^0.9.9",
80
80
  "dot": "^1.1.3",
81
81
  "ejs": "^5.0.2",
@@ -83,7 +83,7 @@
83
83
  "linkedom": "^0.18.12",
84
84
  "node-html-parser": "^7.1.0",
85
85
  "nunjucks": "^3.2.4",
86
- "playwright": "^1.59.1",
86
+ "playwright": "^1.60.0",
87
87
  "pug": "^3.0.4",
88
88
  "rollup": "^4.60.2",
89
89
  "typescript": "^6.0.3",
@@ -626,7 +626,7 @@ export class AbstractBlock extends AbstractNode {
626
626
  if (anyContext || contextSelector !== 'section') {
627
627
  // optimisation
628
628
  // NOTE dlist items can be null
629
- for (const b of this.blocks.flat()) {
629
+ for (const b of this.blocks.flat(Infinity)) {
630
630
  if (b) b.#findByInternal(selector, result, filter)
631
631
  }
632
632
  }
package/src/document.js CHANGED
@@ -34,7 +34,7 @@ import { XmlSanitizeRx, AttributeEntryPassMacroRx } from './rx.js'
34
34
  import { LF } from './constants.js'
35
35
  import { applyLogging } from './logging.js'
36
36
  import { SyntaxHighlighter, DefaultFactoryProxy } from './syntax_highlighter.js'
37
- import { PreprocessorReader, Cursor } from './reader.js'
37
+ import { Reader, PreprocessorReader, Cursor } from './reader.js'
38
38
  import { Parser } from './parser.js'
39
39
  import { Extensions, Registry } from './extensions.js'
40
40
 
@@ -483,8 +483,8 @@ export class Document extends AbstractBlock {
483
483
  if ((this.doctype = attrs.doctype = parentDoctype) !== DEFAULT_DOCTYPE) {
484
484
  this._updateDoctypeAttributes(DEFAULT_DOCTYPE)
485
485
  }
486
- // Set up reader only parsing is deferred to Document.create() / doc.parse().
487
- this.reader = new PreprocessorReader(this, data, options.cursor)
486
+ // Nested documents use a plain Reader (no include/conditional processing), matching Ruby behaviour.
487
+ this.reader = new Reader(data, options.cursor, { document: this })
488
488
  if (this.sourcemap) this.sourceLocation = this.reader.cursor
489
489
  } else {
490
490
  this.backend = null
package/src/extensions.js CHANGED
@@ -75,6 +75,7 @@ import { MacroNameRx, CC_ANY } from './rx.js'
75
75
  * DSL interface for include processors.
76
76
  *
77
77
  * @typedef {DocumentProcessorDslInterface & {
78
+ * handles(fn: (target: string) => boolean): void;
78
79
  * handles(fn: (doc: Document, target: string) => boolean): void;
79
80
  * }} IncludeProcessorDslInterface
80
81
  */
@@ -125,7 +126,7 @@ import { MacroNameRx, CC_ANY } from './rx.js'
125
126
  * - Called with a single Function argument → stores it as the process block.
126
127
  * - Called with non-Function arguments → invokes the stored process block.
127
128
  *
128
- * The this context inside a stored process function is bound to the processor
129
+ * The `this` context inside a stored process function is bound to the processor
129
130
  * instance at definition time.
130
131
  */
131
132
  export const ProcessorDsl = {
@@ -299,6 +300,35 @@ export const SyntaxProcessorDsl = {
299
300
  export const IncludeProcessorDsl = {
300
301
  ...DocumentProcessorDsl,
301
302
 
303
+ /**
304
+ * @overload
305
+ * @param {(target: string) => boolean} fn - Predicate that receives only the include target.
306
+ * @returns {void}
307
+ */
308
+ /**
309
+ * @overload
310
+ * @param {(doc: Document, target: string) => boolean} fn - Predicate that receives the document and the include target.
311
+ * @returns {void}
312
+ */
313
+ /**
314
+ * @overload
315
+ * @param {Document} doc - The document being parsed.
316
+ * @param {string} target - The include target.
317
+ * @returns {boolean}
318
+ */
319
+ /**
320
+ * Register a function that decides whether this include processor handles a given target,
321
+ * or invoke the registered handler.
322
+ *
323
+ * **Setter form** — register a predicate. The callback may accept either just the target
324
+ * string (arity 1) or both the document and the target string (arity 2).
325
+ *
326
+ * **Invoker form** — call the registered predicate with `(doc, target)` and return its
327
+ * result, or return `true` if no predicate has been registered.
328
+ *
329
+ * @param {...*} args
330
+ * @returns {boolean | void}
331
+ */
302
332
  handles(...args) {
303
333
  if (args.length === 1 && typeof args[0] === 'function') {
304
334
  const fn = args[0]
@@ -1072,22 +1102,48 @@ export class ProcessorExtension extends Extension {
1072
1102
  super(kind, instance, instance.config)
1073
1103
  this.processMethod =
1074
1104
  processMethod || ((...args) => instance.process(...args))
1105
+ /** @internal */
1106
+ this._direct = false
1075
1107
  }
1076
1108
  }
1077
1109
 
1078
1110
  // ── Group ─────────────────────────────────────────────────────────────────────
1079
1111
 
1080
1112
  /**
1081
- * A Group registers one or more extensions with a Registry.
1113
+ * A Group bundles one or more extension registrations that are re-executed on
1114
+ * every document conversion, making the registry safe to reuse.
1115
+ *
1116
+ * Subclass Group, override {@link Group#activate}, and either call
1117
+ * {@link Group.register} to add it globally or pass the subclass to
1118
+ * {@link Extensions.create} / {@link Extensions.register}.
1082
1119
  *
1083
- * Subclass Group and pass the subclass to Extensions.register(), or call
1084
- * the static register() method directly.
1120
+ * @example
1121
+ * class MyGroup extends Group {
1122
+ * activate (registry) {
1123
+ * registry.preprocessor(MyPreprocessor)
1124
+ * }
1125
+ * }
1126
+ * MyGroup.register()
1085
1127
  */
1086
1128
  export class Group {
1129
+ /**
1130
+ * Register this Group class globally under the given name.
1131
+ *
1132
+ * Equivalent to calling `Extensions.register(name, this)`.
1133
+ *
1134
+ * @param {string|null} [name] - Optional name for the group.
1135
+ */
1087
1136
  static register(name = null) {
1088
1137
  Extensions.register(name, this)
1089
1138
  }
1090
1139
 
1140
+ /**
1141
+ * Called by {@link Registry#activate} on every document conversion.
1142
+ *
1143
+ * Override this method to register extensions with the provided registry.
1144
+ *
1145
+ * @param {Registry} _registry - The registry to register extensions with.
1146
+ */
1091
1147
  activate(_registry) {
1092
1148
  throw new Error(
1093
1149
  `${this.constructor.name} must implement the activate method`
@@ -1119,10 +1175,24 @@ const SYNTAX_PROCESSOR_CLASSES = {
1119
1175
  * Registry holds the extensions which have been registered and activated, has
1120
1176
  * methods for registering or defining a processor and looks up extensions
1121
1177
  * stored in the registry during parsing.
1178
+ *
1179
+ * A registry can be reused across multiple conversions. Extensions registered
1180
+ * via a group block (passed to {@link Extensions.create} or
1181
+ * {@link Extensions.register}) are re-executed on every activation. Extensions
1182
+ * registered directly on the registry instance (e.g. `registry.preprocessor(fn)`)
1183
+ * are preserved across activations.
1184
+ *
1185
+ * @example
1186
+ * const registry = Extensions.create('my-ext', function () {
1187
+ * this.preprocessor(function () { ... })
1188
+ * })
1189
+ * // registry can be passed to multiple conversions safely
1122
1190
  */
1123
1191
  export class Registry {
1124
1192
  constructor(groups = {}) {
1125
1193
  this.groups = groups
1194
+ /** @internal */
1195
+ this._activating = false
1126
1196
  this._reset()
1127
1197
  }
1128
1198
 
@@ -1139,18 +1209,26 @@ export class Registry {
1139
1209
  ...Object.values(Extensions.groups()),
1140
1210
  ...Object.values(this.groups),
1141
1211
  ]
1142
- for (const group of extGroups) {
1143
- if (typeof group === 'function') {
1144
- // Check if it is a class (constructor) with an activate prototype method.
1145
- if (group.prototype && typeof group.prototype.activate === 'function') {
1146
- new group().activate(this)
1147
- } else {
1148
- // Plain function — call in the context of this registry (like instance_exec).
1149
- group.length === 0 ? group.call(this) : group(this)
1212
+ this._activating = true
1213
+ try {
1214
+ for (const group of extGroups) {
1215
+ if (typeof group === 'function') {
1216
+ // Check if it is a class (constructor) with an activate prototype method.
1217
+ if (
1218
+ group.prototype &&
1219
+ typeof group.prototype.activate === 'function'
1220
+ ) {
1221
+ new group().activate(this)
1222
+ } else {
1223
+ // Plain function — call in the context of this registry (like instance_exec).
1224
+ group.length === 0 ? group.call(this) : group(this)
1225
+ }
1226
+ } else if (group && typeof group.activate === 'function') {
1227
+ group.activate(this)
1150
1228
  }
1151
- } else if (group && typeof group.activate === 'function') {
1152
- group.activate(this)
1153
1229
  }
1230
+ } finally {
1231
+ this._activating = false
1154
1232
  }
1155
1233
  return this
1156
1234
  }
@@ -1726,6 +1804,7 @@ export class Registry {
1726
1804
  }
1727
1805
 
1728
1806
  const extension = new ProcessorExtension(kind, processorInstance)
1807
+ extension._direct = !this._activating
1729
1808
  extension.config.position === '>>'
1730
1809
  ? store.unshift(extension)
1731
1810
  : store.push(extension)
@@ -1797,27 +1876,44 @@ export class Registry {
1797
1876
  }
1798
1877
 
1799
1878
  store[name] = new ProcessorExtension(kind, processorInstance)
1879
+ store[name]._direct = !this._activating
1800
1880
  return store[name]
1801
1881
  }
1802
1882
 
1803
1883
  /** @internal */
1804
1884
  _reset() {
1885
+ // Keep extensions registered directly (outside a group); only clear group-registered ones.
1886
+ // Extensions tagged with _direct=true survive across activations.
1887
+ const keepArr = (arr) => {
1888
+ const kept = arr?.filter((e) => e._direct) ?? []
1889
+ return kept.length ? kept : null
1890
+ }
1891
+ const keepMap = (map) => {
1892
+ if (!map) return null
1893
+ const kept = Object.create(null)
1894
+ for (const [k, v] of Object.entries(map)) if (v._direct) kept[k] = v
1895
+ return Object.keys(kept).length ? kept : null
1896
+ }
1805
1897
  /** @internal */
1806
- this._preprocessor_extensions = null
1898
+ this._preprocessor_extensions = keepArr(this._preprocessor_extensions)
1807
1899
  /** @internal */
1808
- this._tree_processor_extensions = null
1900
+ this._tree_processor_extensions = keepArr(this._tree_processor_extensions)
1809
1901
  /** @internal */
1810
- this._postprocessor_extensions = null
1902
+ this._postprocessor_extensions = keepArr(this._postprocessor_extensions)
1811
1903
  /** @internal */
1812
- this._include_processor_extensions = null
1904
+ this._include_processor_extensions = keepArr(
1905
+ this._include_processor_extensions
1906
+ )
1813
1907
  /** @internal */
1814
- this._docinfo_processor_extensions = null
1908
+ this._docinfo_processor_extensions = keepArr(
1909
+ this._docinfo_processor_extensions
1910
+ )
1815
1911
  /** @internal */
1816
- this._block_extensions = null
1912
+ this._block_extensions = keepMap(this._block_extensions)
1817
1913
  /** @internal */
1818
- this._block_macro_extensions = null
1914
+ this._block_macro_extensions = keepMap(this._block_macro_extensions)
1819
1915
  /** @internal */
1820
- this._inline_macro_extensions = null
1916
+ this._inline_macro_extensions = keepMap(this._inline_macro_extensions)
1821
1917
  this.document = null
1822
1918
  }
1823
1919
 
@@ -1900,6 +1996,13 @@ export const Extensions = {
1900
1996
  /**
1901
1997
  * Create a new Registry, optionally pre-populated with a named block.
1902
1998
  *
1999
+ * When a `block` is provided it is stored as a group and re-executed on every
2000
+ * activation, making the registry safe to reuse across multiple conversions.
2001
+ * Without a `block`, any extensions registered directly on the returned registry
2002
+ * (e.g. `registry.preprocessor(fn)`) are stored in transient state that is
2003
+ * cleared on every activation — those registrations will be lost from the second
2004
+ * conversion onwards. Prefer the block form when the registry may be reused.
2005
+ *
1903
2006
  * @param {string|null} [name=null] - Optional name for the group; auto-generated if omitted.
1904
2007
  * @param {Function|null} [block=null] - Optional function to register as the group.
1905
2008
  * @returns {Registry}
package/src/parser.js CHANGED
@@ -1284,8 +1284,8 @@ export class Parser {
1284
1284
  const admStyle = attributes.style ?? blockContext
1285
1285
  attributes.name = admStyle.toLowerCase()
1286
1286
  attributes.textlabel =
1287
- (attributes.caption && delete attributes.caption) ??
1288
- docAttrs[`${attributes.name}-caption`]
1287
+ attributes.caption ?? docAttrs[`${attributes.name}-caption`]
1288
+ delete attributes.caption
1289
1289
  block = await Parser.buildBlock(
1290
1290
  blockContext,
1291
1291
  'compound',
package/src/table.js CHANGED
@@ -14,6 +14,7 @@ import { Inline } from './inline.js'
14
14
  import { applyLogging } from './logging.js'
15
15
  import { LF, ATTR_REF_HEAD, BASIC_SUBS, NORMAL_SUBS } from './constants.js'
16
16
  import { BlankLineRx, LeadingInlineAnchorRx } from './rx.js'
17
+ import { PreprocessorReader } from './reader.js'
17
18
 
18
19
  /**
19
20
  * Truncate a float to `precision` decimal places (like Ruby's Float#truncate).
@@ -474,6 +475,25 @@ class Cell extends AbstractBlock {
474
475
  if (cell._innerDocSetup) {
475
476
  const { lines, parentDoc, parentDoctitle, options } = cell._innerDocSetup
476
477
  cell._innerDocSetup = null
478
+ // If the first line may be a preprocessor directive (include, ifdef…), expand it using a
479
+ // temporary PreprocessorReader — matching the Ruby behaviour in table.rb.
480
+ if (lines.length > 0 && lines[0].includes('::')) {
481
+ const firstLine = lines[0]
482
+ const tmpReader = new PreprocessorReader(
483
+ parentDoc,
484
+ [firstLine],
485
+ options.cursor
486
+ )
487
+ const preprocessedLines = await tmpReader.readLines()
488
+ if (
489
+ !(
490
+ preprocessedLines.length === 1 && preprocessedLines[0] === firstLine
491
+ )
492
+ ) {
493
+ lines.shift()
494
+ if (preprocessedLines.length > 0) lines.unshift(...preprocessedLines)
495
+ }
496
+ }
477
497
  const innerDoc = await parentDoc.constructor.create(lines, options)
478
498
  if (parentDoctitle) parentDoc.attributes.doctitle = parentDoctitle
479
499
  cell._innerDocument = innerDoc
@@ -489,7 +489,7 @@ export namespace Document {
489
489
  import { Footnote } from './footnote.js';
490
490
  import { AttributeEntry } from './attribute_entry.js';
491
491
  import { AbstractBlock } from './abstract_block.js';
492
- import type { Reader } from './reader.js';
492
+ import { Reader } from './reader.js';
493
493
  import { Section } from './section.js';
494
494
  import { Inline } from './inline.js';
495
495
  export { Footnote, AttributeEntry };
@@ -38,7 +38,25 @@ export namespace SyntaxProcessorDsl {
38
38
  function resolvesAttributes(...args: any[]): void;
39
39
  }
40
40
  export namespace IncludeProcessorDsl {
41
- function handles(...args: any[]): any;
41
+ /**
42
+ * @overload
43
+ * @param {(target: string) => boolean} fn - Predicate that receives only the include target.
44
+ * @returns {void}
45
+ */
46
+ function handles(fn: (target: string) => boolean): void;
47
+ /**
48
+ * @overload
49
+ * @param {(doc: Document, target: string) => boolean} fn - Predicate that receives the document and the include target.
50
+ * @returns {void}
51
+ */
52
+ function handles(fn: (doc: Document, target: string) => boolean): void;
53
+ /**
54
+ * @overload
55
+ * @param {Document} doc - The document being parsed.
56
+ * @param {string} target - The include target.
57
+ * @returns {boolean}
58
+ */
59
+ function handles(doc: Document, target: string): boolean;
42
60
  }
43
61
  export namespace DocinfoProcessorDsl {
44
62
  function atLocation(value: any): void;
@@ -486,14 +504,38 @@ export class ProcessorExtension extends Extension {
486
504
  processMethod: any;
487
505
  }
488
506
  /**
489
- * A Group registers one or more extensions with a Registry.
507
+ * A Group bundles one or more extension registrations that are re-executed on
508
+ * every document conversion, making the registry safe to reuse.
490
509
  *
491
- * Subclass Group and pass the subclass to Extensions.register(), or call
492
- * the static register() method directly.
510
+ * Subclass Group, override {@link Group#activate}, and either call
511
+ * {@link Group.register} to add it globally or pass the subclass to
512
+ * {@link Extensions.create} / {@link Extensions.register}.
513
+ *
514
+ * @example
515
+ * class MyGroup extends Group {
516
+ * activate (registry) {
517
+ * registry.preprocessor(MyPreprocessor)
518
+ * }
519
+ * }
520
+ * MyGroup.register()
493
521
  */
494
522
  export class Group {
495
- static register(name?: any): void;
496
- activate(_registry: any): void;
523
+ /**
524
+ * Register this Group class globally under the given name.
525
+ *
526
+ * Equivalent to calling `Extensions.register(name, this)`.
527
+ *
528
+ * @param {string|null} [name] - Optional name for the group.
529
+ */
530
+ static register(name?: string | null): void;
531
+ /**
532
+ * Called by {@link Registry#activate} on every document conversion.
533
+ *
534
+ * Override this method to register extensions with the provided registry.
535
+ *
536
+ * @param {Registry} _registry - The registry to register extensions with.
537
+ */
538
+ activate(_registry: Registry): void;
497
539
  }
498
540
  /**
499
541
  * The primary entry point into the extension system.
@@ -501,6 +543,18 @@ export class Group {
501
543
  * Registry holds the extensions which have been registered and activated, has
502
544
  * methods for registering or defining a processor and looks up extensions
503
545
  * stored in the registry during parsing.
546
+ *
547
+ * A registry can be reused across multiple conversions. Extensions registered
548
+ * via a group block (passed to {@link Extensions.create} or
549
+ * {@link Extensions.register}) are re-executed on every activation. Extensions
550
+ * registered directly on the registry instance (e.g. `registry.preprocessor(fn)`)
551
+ * are preserved across activations.
552
+ *
553
+ * @example
554
+ * const registry = Extensions.create('my-ext', function () {
555
+ * this.preprocessor(function () { ... })
556
+ * })
557
+ * // registry can be passed to multiple conversions safely
504
558
  */
505
559
  export class Registry {
506
560
  constructor(groups?: {});
@@ -826,6 +880,13 @@ export namespace Extensions {
826
880
  /**
827
881
  * Create a new Registry, optionally pre-populated with a named block.
828
882
  *
883
+ * When a `block` is provided it is stored as a group and re-executed on every
884
+ * activation, making the registry safe to reuse across multiple conversions.
885
+ * Without a `block`, any extensions registered directly on the returned registry
886
+ * (e.g. `registry.preprocessor(fn)`) are stored in transient state that is
887
+ * cleared on every activation — those registrations will be lost from the second
888
+ * conversion onwards. Prefer the block form when the registry may be reused.
889
+ *
829
890
  * @param {string|null} [name=null] - Optional name for the group; auto-generated if omitted.
830
891
  * @param {Function|null} [block=null] - Optional function to register as the group.
831
892
  * @returns {Registry}
@@ -1032,6 +1093,7 @@ export type SyntaxProcessorDslInterface = ProcessorDslInterface & {
1032
1093
  * DSL interface for include processors.
1033
1094
  */
1034
1095
  export type IncludeProcessorDslInterface = DocumentProcessorDslInterface & {
1096
+ handles(fn: (target: string) => boolean): void;
1035
1097
  handles(fn: (doc: Document, target: string) => boolean): void;
1036
1098
  };
1037
1099
  /**