@asciidoctor/core 4.0.0-alpha.1 → 4.0.0-alpha.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,4 +1,4 @@
1
- var version = "4.0.0-alpha.1";
1
+ var version = "4.0.0-alpha.3";
2
2
  const packageJson = {
3
3
  version: version};
4
4
 
@@ -1507,6 +1507,65 @@ function resolveSeverity(severity) {
1507
1507
  return severity ?? Severity.UNKNOWN
1508
1508
  }
1509
1509
 
1510
+ // ── Per-execution logger context ─────────────────────────────────────────────
1511
+
1512
+ // Holds an AsyncLocalStorage instance once it is lazily initialised.
1513
+ // Allows per-execution logger isolation without mutating the global singleton,
1514
+ // making concurrent test execution (e.g. Deno's node:test) safe.
1515
+ let _loggerStore = null;
1516
+
1517
+ // Promise singleton — ensures AsyncLocalStorage is initialised at most once.
1518
+ let _loggerStorePromise = null;
1519
+
1520
+ /**
1521
+ * Lazily initialise an AsyncLocalStorage for per-execution logger context.
1522
+ * Falls back to null in environments that do not support node:async_hooks (e.g. browsers).
1523
+ * @returns {Promise<import('node:async_hooks').AsyncLocalStorage|null>}
1524
+ */
1525
+ async function _ensureLoggerStore() {
1526
+ if (_loggerStorePromise === null) {
1527
+ _loggerStorePromise = import('node:async_hooks')
1528
+ .then(({ AsyncLocalStorage }) => {
1529
+ const store = new AsyncLocalStorage();
1530
+ _loggerStore = store;
1531
+ return store
1532
+ })
1533
+ .catch(() => null);
1534
+ }
1535
+ return _loggerStorePromise
1536
+ }
1537
+
1538
+ /** @internal — returns the logger bound to the current async context, or null */
1539
+ function getContextLogger() {
1540
+ return _loggerStore?.getStore() ?? null
1541
+ }
1542
+
1543
+ /**
1544
+ * Run fn() within an async-local logger context so that all log calls via
1545
+ * `this.logger` (from applyLogging) automatically route to the provided logger
1546
+ * for the duration of the async execution chain.
1547
+ *
1548
+ * Falls back to global mutation in environments without node:async_hooks (e.g. browsers).
1549
+ *
1550
+ * @param {Logger|MemoryLogger|NullLogger} logger - The logger to activate.
1551
+ * @param {() => any} fn - The function to execute within the logger context.
1552
+ * @returns {Promise<any>}
1553
+ */
1554
+ async function withLogger(logger, fn) {
1555
+ const store = await _ensureLoggerStore();
1556
+ if (store) {
1557
+ return store.run(logger, fn)
1558
+ }
1559
+ // Fallback for environments without node:async_hooks (browsers).
1560
+ const prev = LoggerManager.logger;
1561
+ if (logger !== prev) LoggerManager.logger = logger;
1562
+ try {
1563
+ return await fn()
1564
+ } finally {
1565
+ if (logger !== prev) LoggerManager.logger = prev;
1566
+ }
1567
+ }
1568
+
1510
1569
  // ── Logger ────────────────────────────────────────────────────────────────────
1511
1570
 
1512
1571
  /** Standard logger that writes formatted messages to stderr or a custom pipe. */
@@ -1815,8 +1874,9 @@ class MemoryLogger {
1815
1874
  // ── NullLogger ────────────────────────────────────────────────────────────────
1816
1875
 
1817
1876
  /** Logger that discards all messages but still tracks the maximum severity. */
1818
- class NullLogger {
1877
+ class NullLogger extends Logger {
1819
1878
  constructor() {
1879
+ super();
1820
1880
  this.level = Severity.UNKNOWN;
1821
1881
  this._maxSeverity = null;
1822
1882
  }
@@ -1961,12 +2021,12 @@ const LoggerManager = (() => {
1961
2021
  function applyLogging(proto) {
1962
2022
  Object.defineProperty(proto, 'logger', {
1963
2023
  get() {
1964
- return LoggerManager.logger
2024
+ return _loggerStore?.getStore() ?? LoggerManager.logger
1965
2025
  },
1966
2026
  configurable: true,
1967
2027
  });
1968
2028
 
1969
- proto.getLogger = () => LoggerManager.logger;
2029
+ proto.getLogger = () => _loggerStore?.getStore() ?? LoggerManager.logger;
1970
2030
 
1971
2031
  proto.messageWithContext = (text, context = {}) =>
1972
2032
  Logger.AutoFormattingMessage.attach({ text, ...context });
@@ -2013,7 +2073,16 @@ const rstrip = (line) => line.replace(/[ \t\r\n\f\v]+$/, '');
2013
2073
  */
2014
2074
  function prepareSourceArray(data, trimEnd = true) {
2015
2075
  if (!data.length) return []
2016
- if (data[0].startsWith(BOM)) data[0] = data[0].slice(1);
2076
+ if (data[0].startsWith(BOM)) {
2077
+ data[0] = data[0].slice(1);
2078
+ } else if (
2079
+ data[0].charCodeAt(0) === 0xef &&
2080
+ data[0].charCodeAt(1) === 0xbb &&
2081
+ data[0].charCodeAt(2) === 0xbf
2082
+ ) {
2083
+ // Strip UTF-8 BOM encoded as three raw characters ( / \xEF\xBB\xBF) if not already decoded to U+FEFF
2084
+ data[0] = data[0].slice(3);
2085
+ }
2017
2086
  // Strip trailing \r to normalize Windows CRLF line endings (lines were split on \n, leaving \r).
2018
2087
  return trimEnd
2019
2088
  ? data.map(rstrip)
@@ -2033,7 +2102,16 @@ function prepareSourceArray(data, trimEnd = true) {
2033
2102
  */
2034
2103
  function prepareSourceString(data, trimEnd = true) {
2035
2104
  if (!data) return []
2036
- if (data.startsWith(BOM)) data = data.slice(1);
2105
+ if (data.startsWith(BOM)) {
2106
+ data = data.slice(1);
2107
+ } else if (
2108
+ data.charCodeAt(0) === 0xef &&
2109
+ data.charCodeAt(1) === 0xbb &&
2110
+ data.charCodeAt(2) === 0xbf
2111
+ ) {
2112
+ // Strip UTF-8 BOM encoded as three raw characters ( / \xEF\xBB\xBF) if not already decoded to U+FEFF
2113
+ data = data.slice(3);
2114
+ }
2037
2115
  // Normalize Windows CRLF to LF so that split('\n') does not leave trailing \r on each line.
2038
2116
  if (data.includes('\r'))
2039
2117
  data = data.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
@@ -3743,7 +3821,7 @@ class AbstractBlock extends AbstractNode {
3743
3821
  * - `'reject'` → skip the node and its children
3744
3822
  * - `'stop'` → include the node (if it matched) and stop the entire traversal
3745
3823
  *
3746
- * @param {Object} [selector={}] - Selector criteria object.
3824
+ * @param {Object|Function} [selector={}] - Selector criteria object, or a filter callback when called as `findBy(callback)`.
3747
3825
  * @param {Function|null} [filter=null] - Per-node filter callback.
3748
3826
  * @returns {AbstractBlock[]} array of matching block-level nodes.
3749
3827
  *
@@ -3758,6 +3836,9 @@ class AbstractBlock extends AbstractNode {
3758
3836
  *
3759
3837
  * @example <caption>All image blocks including those inside AsciiDoc table cells</caption>
3760
3838
  * const images = doc.findBy({ context: 'image', traverseDocuments: true })
3839
+ *
3840
+ * @example <caption>Filter-only shorthand (no selector)</caption>
3841
+ * const verbatim = doc.findBy((b) => b.contentModel === ContentModel.VERBATIM)
3761
3842
  */
3762
3843
  findBy(selector = {}, filter = null) {
3763
3844
  const result = [];
@@ -3766,8 +3847,13 @@ class AbstractBlock extends AbstractNode {
3766
3847
  selector && typeof selector === 'object' && !Array.isArray(selector)
3767
3848
  ? selector
3768
3849
  : {};
3769
- // Normalise: filter must be a function; ignore string/non-function values (mirrors core API).
3770
- const normFilter = typeof filter === 'function' ? filter : null;
3850
+ // Normalise: support findBy(callback) shorthand selector is the filter when it's a function.
3851
+ const normFilter =
3852
+ typeof filter === 'function'
3853
+ ? filter
3854
+ : typeof selector === 'function'
3855
+ ? selector
3856
+ : null;
3771
3857
  try {
3772
3858
  this.#findByInternal(normSelector, result, normFilter);
3773
3859
  } catch (e) {
@@ -5957,7 +6043,7 @@ const SyntaxHighlighter = new DefaultFactory();
5957
6043
  //
5958
6044
  // This logic is specific to Asciidoctor.js and has no equivalent in the upstream Ruby asciidoctor
5959
6045
  // implementation. It handles the case where the document is loaded in a browser environment
5960
- // (XMLHttpRequest / Fetch IO module) where paths can be file:// or http(s):// URIs.
6046
+ // where paths can be file:// or http(s):// URIs.
5961
6047
  //
5962
6048
  // The key behavioural differences from the standard file-system resolver:
5963
6049
  // - Relative targets are resolved by string concatenation against a URI context dir,
@@ -6006,7 +6092,7 @@ function _linkReplacement(reader, target, attrlist) {
6006
6092
  * 1. target starts with file:// → inc_path = relpath = target
6007
6093
  * 2. target is a URI → must descend from baseDir or allow-uri-read; else → link
6008
6094
  * 3. target is an absolute OS path → prepend file:// (or file:///)
6009
- * 4. baseDir == '.' → inc_path = relpath = target (resolved by XMLHttpRequest/fetch)
6095
+ * 4. baseDir == '.' → inc_path = relpath = target (resolved by fetch)
6010
6096
  * 5. baseDir starts with file:// OR baseDir is not a URI → inc_path = baseDir/target; relpath = target
6011
6097
  * 6. baseDir is an absolute URL → inc_path = baseDir/target; relpath = target
6012
6098
  *
@@ -6728,7 +6814,7 @@ class Reader {
6728
6814
  return this.source()
6729
6815
  }
6730
6816
  getLogger() {
6731
- return LoggerManager.logger
6817
+ return this._document?.logger ?? LoggerManager.logger
6732
6818
  }
6733
6819
  createLogMessage(text, context = {}) {
6734
6820
  return Logger.AutoFormattingMessage.attach({ text, ...context })
@@ -9553,6 +9639,22 @@ class AttributeList {
9553
9639
  }
9554
9640
  }
9555
9641
 
9642
+ // Symbol key used to store attribute entries on a block-attributes object without
9643
+ // polluting the public attributes (invisible to Object.keys / for-in / JSON.stringify).
9644
+ // Spread ({ ...attrs }) copies Symbol-keyed properties, so the entry survives the
9645
+ // shallow clone made in AbstractNode's constructor.
9646
+ const ATTR_ENTRIES_KEY = Symbol('attribute_entries');
9647
+
9648
+ /**
9649
+ * Return the attribute entries stored for the given block attributes object,
9650
+ * or undefined if none have been saved.
9651
+ * @param {Object} blockAttributes
9652
+ * @returns {AttributeEntry[]|undefined}
9653
+ */
9654
+ function getAttributeEntries(blockAttributes) {
9655
+ return blockAttributes[ATTR_ENTRIES_KEY]
9656
+ }
9657
+
9556
9658
  class AttributeEntry {
9557
9659
  constructor(name, value, negate = null) {
9558
9660
  this.name = name;
@@ -9561,7 +9663,7 @@ class AttributeEntry {
9561
9663
  }
9562
9664
 
9563
9665
  saveTo(blockAttributes) {
9564
- (blockAttributes.attribute_entries ??= []).push(this);
9666
+ (blockAttributes[ATTR_ENTRIES_KEY] ??= []).push(this);
9565
9667
  return this
9566
9668
  }
9567
9669
  }
@@ -11036,7 +11138,7 @@ class Parser {
11036
11138
  const Rdr = Reader;
11037
11139
  const result = await extension.processMethod(
11038
11140
  parent,
11039
- blockReader ?? new Rdr(lines),
11141
+ blockReader ?? new Rdr(lines, null, { document: parent.document }),
11040
11142
  { ...attributes }
11041
11143
  );
11042
11144
  if (!result || result === parent) return null
@@ -13158,6 +13260,94 @@ function _uniform(str, chr, len) {
13158
13260
  // references; they will be resolved when those modules implement the methods.
13159
13261
 
13160
13262
 
13263
+ // Type-only imports for JSDoc
13264
+ /**
13265
+ * @typedef {import('./document.js').Document} Document
13266
+ * @typedef {import('./abstract_block.js').AbstractBlock} AbstractBlock
13267
+ */
13268
+
13269
+ // ── DSL interface types ───────────────────────────────────────────────────────
13270
+
13271
+ /**
13272
+ * DSL interface for configuring a {@link Processor} instance.
13273
+ * Applied to a processor instance via `Object.assign(instance, DslMixin)`.
13274
+ *
13275
+ * The `process` property behaves as a setter when called with a single Function
13276
+ * argument (stores the process block), or as a passthrough caller otherwise.
13277
+ *
13278
+ * @typedef {object} ProcessorDslInterface
13279
+ * @property {(key: string, value: unknown) => void} option - Set a config option.
13280
+ * @property {(fn: (...args: unknown[]) => unknown) => void} process - Register the process function.
13281
+ * @property {() => boolean} processBlockGiven - Returns true if a process function has been registered.
13282
+ */
13283
+
13284
+ /**
13285
+ * DSL interface for document processors (Preprocessor, TreeProcessor, Postprocessor, DocinfoProcessor).
13286
+ *
13287
+ * @typedef {ProcessorDslInterface & { prefer(): void; prepend(): void }} DocumentProcessorDslInterface
13288
+ */
13289
+
13290
+ /**
13291
+ * DSL interface for syntax processors (BlockProcessor, BlockMacroProcessor, InlineMacroProcessor).
13292
+ *
13293
+ * @typedef {ProcessorDslInterface & {
13294
+ * named(value: string): void;
13295
+ * contentModel(value: string): void;
13296
+ * parseContentAs(value: string): void;
13297
+ * positionalAttributes(...value: string[]): void;
13298
+ * namePositionalAttributes(...value: string[]): void;
13299
+ * positionalAttrs(...value: string[]): void;
13300
+ * defaultAttributes(value: Record<string, string>): void;
13301
+ * defaultAttrs(value: Record<string, string>): void;
13302
+ * resolveAttributes(...args: unknown[]): void;
13303
+ * resolvesAttributes(...args: unknown[]): void;
13304
+ * }} SyntaxProcessorDslInterface
13305
+ */
13306
+
13307
+ /**
13308
+ * DSL interface for include processors.
13309
+ *
13310
+ * @typedef {DocumentProcessorDslInterface & {
13311
+ * handles(fn: (doc: Document, target: string) => boolean): void;
13312
+ * }} IncludeProcessorDslInterface
13313
+ */
13314
+
13315
+ /**
13316
+ * DSL interface for docinfo processors.
13317
+ *
13318
+ * @typedef {DocumentProcessorDslInterface & {
13319
+ * atLocation(value: string): void;
13320
+ * }} DocinfoProcessorDslInterface
13321
+ */
13322
+
13323
+ /**
13324
+ * DSL interface for block processors.
13325
+ *
13326
+ * @typedef {SyntaxProcessorDslInterface & {
13327
+ * contexts(...value: (string | string[])[]): void;
13328
+ * onContexts(...value: (string | string[])[]): void;
13329
+ * onContext(...value: (string | string[])[]): void;
13330
+ * bindTo(...value: (string | string[])[]): void;
13331
+ * }} BlockProcessorDslInterface
13332
+ */
13333
+
13334
+ /**
13335
+ * DSL interface for macro processors (block and inline macros).
13336
+ *
13337
+ * @typedef {SyntaxProcessorDslInterface} MacroProcessorDslInterface
13338
+ */
13339
+
13340
+ /**
13341
+ * DSL interface for inline macro processors.
13342
+ *
13343
+ * @typedef {MacroProcessorDslInterface & {
13344
+ * format(value: string): void;
13345
+ * matchFormat(value: string): void;
13346
+ * usingFormat(value: string): void;
13347
+ * match(value: RegExp): void;
13348
+ * }} InlineMacroProcessorDslInterface
13349
+ */
13350
+
13161
13351
  // ── DSL Mixins ────────────────────────────────────────────────────────────────
13162
13352
 
13163
13353
  /**
@@ -13753,7 +13943,12 @@ class Processor {
13753
13943
  * Extensions.register(function () { this.preprocessor(CommentStripPreprocessor) })
13754
13944
  */
13755
13945
  class Preprocessor extends Processor {
13756
- process(_document, _reader) {
13946
+ /**
13947
+ * @param {Document} document - The document being parsed.
13948
+ * @param {Reader} reader - The reader positioned at the beginning of the source.
13949
+ * @returns {Reader|undefined} The same or a substitute Reader, or undefined to use the original.
13950
+ */
13951
+ process(document, reader) {
13757
13952
  throw new Error(
13758
13953
  `${this.constructor.name} must implement the process method`
13759
13954
  )
@@ -13778,7 +13973,11 @@ Preprocessor.DSL = DocumentProcessorDsl;
13778
13973
  * Extensions.register(function () { this.treeProcessor(ShoutTreeProcessor) })
13779
13974
  */
13780
13975
  class TreeProcessor extends Processor {
13781
- process(_document) {
13976
+ /**
13977
+ * @param {Document} document - The parsed document.
13978
+ * @returns {void}
13979
+ */
13980
+ process(document) {
13782
13981
  throw new Error(
13783
13982
  `${this.constructor.name} must implement the process method`
13784
13983
  )
@@ -13804,7 +14003,12 @@ TreeProcessor.DSL = DocumentProcessorDsl;
13804
14003
  * Extensions.register(function () { this.postprocessor(FooterPostprocessor) })
13805
14004
  */
13806
14005
  class Postprocessor extends Processor {
13807
- process(_document, _output) {
14006
+ /**
14007
+ * @param {Document} document - The converted document.
14008
+ * @param {string} output - The converted output string.
14009
+ * @returns {string} The (possibly modified) output string.
14010
+ */
14011
+ process(document, output) {
13808
14012
  throw new Error(
13809
14013
  `${this.constructor.name} must implement the process method`
13810
14014
  )
@@ -13818,13 +14022,25 @@ Postprocessor.DSL = DocumentProcessorDsl;
13818
14022
  * Implementations must extend IncludeProcessor.
13819
14023
  */
13820
14024
  class IncludeProcessor extends Processor {
13821
- process(_document, _reader, _target, _attributes) {
14025
+ /**
14026
+ * @param {Document} document - The document being parsed.
14027
+ * @param {Reader} reader - The reader for the including document.
14028
+ * @param {string} target - The target of the include directive.
14029
+ * @param {Record<string, string>} attributes - The parsed include attributes.
14030
+ * @returns {void}
14031
+ */
14032
+ process(document, reader, target, attributes) {
13822
14033
  throw new Error(
13823
14034
  `${this.constructor.name} must implement the process method`
13824
14035
  )
13825
14036
  }
13826
14037
 
13827
- handles(_doc, _target) {
14038
+ /**
14039
+ * @param {Document} doc - The document being parsed.
14040
+ * @param {string} target - The target of the include directive.
14041
+ * @returns {boolean} true if this processor handles the given target.
14042
+ */
14043
+ handles(doc, target) {
13828
14044
  return true
13829
14045
  }
13830
14046
  }
@@ -13842,7 +14058,11 @@ class DocinfoProcessor extends Processor {
13842
14058
  this.config.location ??= 'head';
13843
14059
  }
13844
14060
 
13845
- process(_document) {
14061
+ /**
14062
+ * @param {Document} document - The document being converted.
14063
+ * @returns {string} The docinfo content to inject into the document.
14064
+ */
14065
+ process(document) {
13846
14066
  throw new Error(
13847
14067
  `${this.constructor.name} must implement the process method`
13848
14068
  )
@@ -13898,7 +14118,13 @@ class BlockProcessor extends Processor {
13898
14118
  this.config.content_model ??= 'compound';
13899
14119
  }
13900
14120
 
13901
- process(_parent, _reader, _attributes) {
14121
+ /**
14122
+ * @param {AbstractBlock} parent - The enclosing block.
14123
+ * @param {Reader} reader - The reader positioned at the block content.
14124
+ * @param {Record<string, unknown>} attributes - The parsed block attributes.
14125
+ * @returns {Block|void} A block node, or void to let the parser handle it.
14126
+ */
14127
+ process(parent, reader, attributes) {
13902
14128
  throw new Error(
13903
14129
  `${this.constructor.name} must implement the process method`
13904
14130
  )
@@ -13916,7 +14142,13 @@ class MacroProcessor extends Processor {
13916
14142
  this.config.content_model ??= 'attributes';
13917
14143
  }
13918
14144
 
13919
- process(_parent, _target, _attributes) {
14145
+ /**
14146
+ * @param {AbstractBlock} parent - The enclosing block.
14147
+ * @param {string} target - The macro target (text between `name:` and `[`).
14148
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
14149
+ * @returns {Block|Inline|void}
14150
+ */
14151
+ process(parent, target, attributes) {
13920
14152
  throw new Error(
13921
14153
  `${this.constructor.name} must implement the process method`
13922
14154
  )
@@ -13962,6 +14194,16 @@ class BlockMacroProcessor extends MacroProcessor {
13962
14194
  set name(value) {
13963
14195
  this._name = value;
13964
14196
  }
14197
+
14198
+ /**
14199
+ * @param {AbstractBlock} parent - The enclosing block.
14200
+ * @param {string} target - The macro target.
14201
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
14202
+ * @returns {Block} A block node created with one of the `createBlock` helpers.
14203
+ */
14204
+ process(parent, target, attributes) {
14205
+ return super.process(parent, target, attributes)
14206
+ }
13965
14207
  }
13966
14208
  BlockMacroProcessor.DSL = MacroProcessorDsl;
13967
14209
 
@@ -14005,6 +14247,16 @@ class InlineMacroProcessor extends MacroProcessor {
14005
14247
  ))
14006
14248
  }
14007
14249
 
14250
+ /**
14251
+ * @param {AbstractBlock} parent - The enclosing block.
14252
+ * @param {string} target - The macro target.
14253
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
14254
+ * @returns {Inline} An Inline node created with `this.createInline(...)`.
14255
+ */
14256
+ process(parent, target, attributes) {
14257
+ return super.process(parent, target, attributes)
14258
+ }
14259
+
14008
14260
  resolveRegexp(name, format) {
14009
14261
  if (!MacroNameRx.test(name)) {
14010
14262
  throw new Error(`invalid name for inline macro: ${name}`)
@@ -14368,6 +14620,15 @@ class Registry {
14368
14620
  return !!this._block_extensions
14369
14621
  }
14370
14622
 
14623
+ /**
14624
+ * Retrieve all BlockProcessor Extension proxy objects.
14625
+ *
14626
+ * @returns {ProcessorExtension[]}
14627
+ */
14628
+ blocks() {
14629
+ return this._block_extensions ? Object.values(this._block_extensions) : []
14630
+ }
14631
+
14371
14632
  /**
14372
14633
  * Check whether a BlockProcessor is registered for the given name and context.
14373
14634
  *
@@ -14414,6 +14675,17 @@ class Registry {
14414
14675
  return !!this._block_macro_extensions
14415
14676
  }
14416
14677
 
14678
+ /**
14679
+ * Retrieve all BlockMacroProcessor Extension proxy objects.
14680
+ *
14681
+ * @returns {ProcessorExtension[]}
14682
+ */
14683
+ blockMacros() {
14684
+ return this._block_macro_extensions
14685
+ ? Object.values(this._block_macro_extensions)
14686
+ : []
14687
+ }
14688
+
14417
14689
  /**
14418
14690
  * Check whether a BlockMacroProcessor is registered for the given name.
14419
14691
  *
@@ -14519,6 +14791,85 @@ class Registry {
14519
14791
  return extension
14520
14792
  }
14521
14793
 
14794
+ // ── JavaScript-style accessors ───────────────────────────────────────────────
14795
+
14796
+ /** @returns {object} the plain Object that maps names to groups for this registry. */
14797
+ getGroups() {
14798
+ return this.groups
14799
+ }
14800
+
14801
+ /** Alias for {@link preprocessors}. */
14802
+ getPreprocessors() {
14803
+ return this.preprocessors()
14804
+ }
14805
+
14806
+ /** Alias for {@link treeProcessors}. */
14807
+ getTreeProcessors() {
14808
+ return this.treeProcessors()
14809
+ }
14810
+
14811
+ /** Alias for {@link includeProcessors}. */
14812
+ getIncludeProcessors() {
14813
+ return this.includeProcessors()
14814
+ }
14815
+
14816
+ /** Alias for {@link postprocessors}. */
14817
+ getPostprocessors() {
14818
+ return this.postprocessors()
14819
+ }
14820
+
14821
+ /**
14822
+ * Alias for {@link docinfoProcessors}.
14823
+ *
14824
+ * @param {string|null} [location=null]
14825
+ */
14826
+ getDocinfoProcessors(location = null) {
14827
+ return this.docinfoProcessors(location)
14828
+ }
14829
+
14830
+ /** Alias for {@link blocks}. */
14831
+ getBlocks() {
14832
+ return this.blocks()
14833
+ }
14834
+
14835
+ /** Alias for {@link blockMacros}. */
14836
+ getBlockMacros() {
14837
+ return this.blockMacros()
14838
+ }
14839
+
14840
+ /** Alias for {@link inlineMacros}. */
14841
+ getInlineMacros() {
14842
+ return this.inlineMacros()
14843
+ }
14844
+
14845
+ /**
14846
+ * Alias for {@link registeredForInlineMacro}.
14847
+ *
14848
+ * @param {string} name
14849
+ */
14850
+ getInlineMacroFor(name) {
14851
+ return this.registeredForInlineMacro(name)
14852
+ }
14853
+
14854
+ /**
14855
+ * Alias for {@link registeredForBlock}.
14856
+ *
14857
+ * @param {string} name
14858
+ * @param {string} context
14859
+ */
14860
+ getBlockFor(name, context) {
14861
+ return this.registeredForBlock(name, context)
14862
+ }
14863
+
14864
+ /**
14865
+ * Alias for {@link registeredForBlockMacro}.
14866
+ *
14867
+ * @param {string} name
14868
+ */
14869
+ getBlockMacroFor(name) {
14870
+ return this.registeredForBlockMacro(name)
14871
+ }
14872
+
14522
14873
  // ── Private helpers ──────────────────────────────────────────────────────────
14523
14874
 
14524
14875
  /** @internal */
@@ -14744,6 +15095,15 @@ const Extensions = {
14744
15095
  return _groups
14745
15096
  },
14746
15097
 
15098
+ /**
15099
+ * Alias for {@link groups}.
15100
+ *
15101
+ * @returns {object}
15102
+ */
15103
+ getGroups() {
15104
+ return this.groups()
15105
+ },
15106
+
14747
15107
  /**
14748
15108
  * Create a new Registry, optionally pre-populated with a named block.
14749
15109
  *
@@ -15971,8 +16331,9 @@ class Document extends AbstractBlock {
15971
16331
  * @param {Object} blockAttributes
15972
16332
  */
15973
16333
  playbackAttributes(blockAttributes) {
15974
- if (!('attribute_entries' in blockAttributes)) return
15975
- for (const entry of blockAttributes.attribute_entries) {
16334
+ const entries = getAttributeEntries(blockAttributes);
16335
+ if (!entries) return
16336
+ for (const entry of entries) {
15976
16337
  if (entry.negate) {
15977
16338
  delete this.attributes[entry.name];
15978
16339
  if (entry.name === 'compat-mode') this.compatMode = false;
@@ -16701,6 +17062,12 @@ class Document extends AbstractBlock {
16701
17062
  * Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
16702
17063
  */
16703
17064
  async _resolveAllTexts(block) {
17065
+ // The header section lives outside document.blocks; pre-compute its title here so
17066
+ // that doc.doctitle() returns the fully-substituted title (with replacements applied,
17067
+ // e.g. ' → &#8217;) rather than the header-subs-only fallback.
17068
+ if (block === this && this.header) {
17069
+ await this.header.precomputeTitle?.();
17070
+ }
16704
17071
  // Skip title pre-computation for blocks with an explicit empty id ([id=]).
16705
17072
  // In Ruby, apply_title_subs is lazy: it is never called during parsing for such
16706
17073
  // blocks because section.title is never accessed. An explicit empty id is
@@ -16784,7 +17151,7 @@ class Document extends AbstractBlock {
16784
17151
  * @internal
16785
17152
  */
16786
17153
  _clearPlaybackAttributes(attributes) {
16787
- delete attributes.attribute_entries;
17154
+ delete attributes[ATTR_ENTRIES_KEY];
16788
17155
  }
16789
17156
 
16790
17157
  /**
@@ -19512,17 +19879,23 @@ async function load$1(input, options = {}) {
19512
19879
  // Shallow-copy options so we don't mutate the caller's object.
19513
19880
  options = Object.assign({}, options);
19514
19881
 
19515
- const timings = options.timings ?? null;
19516
- if (timings) timings.start('read');
19517
-
19518
19882
  // ── Logger override ───────────────────────────────────────────────────────
19883
+ // When a logger option is supplied, run the conversion in an async-local
19884
+ // context so the logger is scoped to this call only — no global mutation,
19885
+ // which makes concurrent callers (e.g. parallel Deno tests) safe.
19519
19886
  if ('logger' in options) {
19520
- const logger = options.logger;
19521
- if (logger !== LoggerManager.logger) {
19522
- LoggerManager.logger = logger ?? new NullLogger();
19523
- }
19887
+ const newLogger = options.logger ?? new NullLogger();
19888
+ delete options.logger;
19889
+ return withLogger(newLogger, () => _doLoad(input, options, newLogger))
19524
19890
  }
19525
19891
 
19892
+ return _doLoad(input, options)
19893
+ }
19894
+
19895
+ async function _doLoad(input, options, explicitLogger = null) {
19896
+ const timings = options.timings ?? null;
19897
+ if (timings) timings.start('read');
19898
+
19526
19899
  // ── Attributes normalisation ──────────────────────────────────────────────
19527
19900
  let attrs = options.attributes;
19528
19901
  if (!attrs) {
@@ -19554,11 +19927,9 @@ async function load$1(input, options = {}) {
19554
19927
  attrs.docname = basename(inputPath, docfilesuffix);
19555
19928
  }
19556
19929
  source = await _readStream(input);
19557
- } else if (
19558
- typeof input === 'object' &&
19559
- input?.constructor?.name === 'Buffer'
19560
- ) {
19561
- source = input.toString('utf8');
19930
+ } else if (input instanceof Uint8Array) {
19931
+ // Covers both Node.js Buffer (a Uint8Array subclass) and browser Uint8Array shims.
19932
+ source = new TextDecoder('utf-8').decode(input);
19562
19933
  } else if (typeof input === 'string') {
19563
19934
  source = input;
19564
19935
  } else if (Array.isArray(input)) {
@@ -19621,6 +19992,19 @@ async function load$1(input, options = {}) {
19621
19992
  }
19622
19993
 
19623
19994
  if (timings) timings.record('parse');
19995
+
19996
+ // Persist the logger on the Document instance so that doc.convert()
19997
+ // (called by convert.js after the async-local context ends) still routes
19998
+ // logging through the caller-supplied logger.
19999
+ // ALS provides it in Node.js/Deno; the explicit parameter covers browser fallback.
20000
+ const contextLogger = getContextLogger() ?? explicitLogger;
20001
+ if (contextLogger) {
20002
+ Object.defineProperty(doc, 'logger', {
20003
+ get: () => contextLogger,
20004
+ configurable: true,
20005
+ });
20006
+ }
20007
+
19624
20008
  return doc
19625
20009
  }
19626
20010
 
@@ -20967,10 +21351,7 @@ ${await node.content()}
20967
21351
  img =
20968
21352
  (await this.readSvgContents(node, target)) ||
20969
21353
  `<span class="alt">${node.getAlt()}</span>`;
20970
- } else if (
20971
- node.hasOption('interactive') &&
20972
- node.document.safe >= SafeMode.SERVER
20973
- ) {
21354
+ } else if (node.hasOption('interactive')) {
20974
21355
  const fallback = node.hasAttribute('fallback')
20975
21356
  ? `<img src="${await node.imageUri(node.getAttribute('fallback'))}" alt="${this._encodeAttrValue(node.getAlt())}"${widthAttr}${heightAttr}${slash}>`
20976
21357
  : `<span class="alt">${node.getAlt()}</span>`;
@@ -21811,10 +22192,7 @@ Your browser does not support the video tag.
21811
22192
  img =
21812
22193
  (await this.readSvgContents(node, target)) ||
21813
22194
  `<span class="alt">${node.getAlt()}</span>`;
21814
- } else if (
21815
- node.hasOption('interactive') &&
21816
- node.document.safe >= SafeMode.SERVER
21817
- ) {
22195
+ } else if (node.hasOption('interactive')) {
21818
22196
  const fallback = node.hasAttribute('fallback')
21819
22197
  ? `<img src="${await node.imageUri(node.getAttribute('fallback'))}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
21820
22198
  : `<span class="alt">${node.getAlt()}</span>`;