@asciidoctor/core 4.0.0-alpha.2 → 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.2";
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');
@@ -5965,7 +6043,7 @@ const SyntaxHighlighter = new DefaultFactory();
5965
6043
  //
5966
6044
  // This logic is specific to Asciidoctor.js and has no equivalent in the upstream Ruby asciidoctor
5967
6045
  // implementation. It handles the case where the document is loaded in a browser environment
5968
- // (XMLHttpRequest / Fetch IO module) where paths can be file:// or http(s):// URIs.
6046
+ // where paths can be file:// or http(s):// URIs.
5969
6047
  //
5970
6048
  // The key behavioural differences from the standard file-system resolver:
5971
6049
  // - Relative targets are resolved by string concatenation against a URI context dir,
@@ -6014,7 +6092,7 @@ function _linkReplacement(reader, target, attrlist) {
6014
6092
  * 1. target starts with file:// → inc_path = relpath = target
6015
6093
  * 2. target is a URI → must descend from baseDir or allow-uri-read; else → link
6016
6094
  * 3. target is an absolute OS path → prepend file:// (or file:///)
6017
- * 4. baseDir == '.' → inc_path = relpath = target (resolved by XMLHttpRequest/fetch)
6095
+ * 4. baseDir == '.' → inc_path = relpath = target (resolved by fetch)
6018
6096
  * 5. baseDir starts with file:// OR baseDir is not a URI → inc_path = baseDir/target; relpath = target
6019
6097
  * 6. baseDir is an absolute URL → inc_path = baseDir/target; relpath = target
6020
6098
  *
@@ -6736,7 +6814,7 @@ class Reader {
6736
6814
  return this.source()
6737
6815
  }
6738
6816
  getLogger() {
6739
- return LoggerManager.logger
6817
+ return this._document?.logger ?? LoggerManager.logger
6740
6818
  }
6741
6819
  createLogMessage(text, context = {}) {
6742
6820
  return Logger.AutoFormattingMessage.attach({ text, ...context })
@@ -9561,6 +9639,22 @@ class AttributeList {
9561
9639
  }
9562
9640
  }
9563
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
+
9564
9658
  class AttributeEntry {
9565
9659
  constructor(name, value, negate = null) {
9566
9660
  this.name = name;
@@ -9569,7 +9663,7 @@ class AttributeEntry {
9569
9663
  }
9570
9664
 
9571
9665
  saveTo(blockAttributes) {
9572
- (blockAttributes.attribute_entries ??= []).push(this);
9666
+ (blockAttributes[ATTR_ENTRIES_KEY] ??= []).push(this);
9573
9667
  return this
9574
9668
  }
9575
9669
  }
@@ -11044,7 +11138,7 @@ class Parser {
11044
11138
  const Rdr = Reader;
11045
11139
  const result = await extension.processMethod(
11046
11140
  parent,
11047
- blockReader ?? new Rdr(lines),
11141
+ blockReader ?? new Rdr(lines, null, { document: parent.document }),
11048
11142
  { ...attributes }
11049
11143
  );
11050
11144
  if (!result || result === parent) return null
@@ -13166,6 +13260,94 @@ function _uniform(str, chr, len) {
13166
13260
  // references; they will be resolved when those modules implement the methods.
13167
13261
 
13168
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
+
13169
13351
  // ── DSL Mixins ────────────────────────────────────────────────────────────────
13170
13352
 
13171
13353
  /**
@@ -13761,7 +13943,12 @@ class Processor {
13761
13943
  * Extensions.register(function () { this.preprocessor(CommentStripPreprocessor) })
13762
13944
  */
13763
13945
  class Preprocessor extends Processor {
13764
- 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) {
13765
13952
  throw new Error(
13766
13953
  `${this.constructor.name} must implement the process method`
13767
13954
  )
@@ -13786,7 +13973,11 @@ Preprocessor.DSL = DocumentProcessorDsl;
13786
13973
  * Extensions.register(function () { this.treeProcessor(ShoutTreeProcessor) })
13787
13974
  */
13788
13975
  class TreeProcessor extends Processor {
13789
- process(_document) {
13976
+ /**
13977
+ * @param {Document} document - The parsed document.
13978
+ * @returns {void}
13979
+ */
13980
+ process(document) {
13790
13981
  throw new Error(
13791
13982
  `${this.constructor.name} must implement the process method`
13792
13983
  )
@@ -13812,7 +14003,12 @@ TreeProcessor.DSL = DocumentProcessorDsl;
13812
14003
  * Extensions.register(function () { this.postprocessor(FooterPostprocessor) })
13813
14004
  */
13814
14005
  class Postprocessor extends Processor {
13815
- 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) {
13816
14012
  throw new Error(
13817
14013
  `${this.constructor.name} must implement the process method`
13818
14014
  )
@@ -13826,13 +14022,25 @@ Postprocessor.DSL = DocumentProcessorDsl;
13826
14022
  * Implementations must extend IncludeProcessor.
13827
14023
  */
13828
14024
  class IncludeProcessor extends Processor {
13829
- 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) {
13830
14033
  throw new Error(
13831
14034
  `${this.constructor.name} must implement the process method`
13832
14035
  )
13833
14036
  }
13834
14037
 
13835
- 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) {
13836
14044
  return true
13837
14045
  }
13838
14046
  }
@@ -13850,7 +14058,11 @@ class DocinfoProcessor extends Processor {
13850
14058
  this.config.location ??= 'head';
13851
14059
  }
13852
14060
 
13853
- 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) {
13854
14066
  throw new Error(
13855
14067
  `${this.constructor.name} must implement the process method`
13856
14068
  )
@@ -13906,7 +14118,13 @@ class BlockProcessor extends Processor {
13906
14118
  this.config.content_model ??= 'compound';
13907
14119
  }
13908
14120
 
13909
- 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) {
13910
14128
  throw new Error(
13911
14129
  `${this.constructor.name} must implement the process method`
13912
14130
  )
@@ -13924,7 +14142,13 @@ class MacroProcessor extends Processor {
13924
14142
  this.config.content_model ??= 'attributes';
13925
14143
  }
13926
14144
 
13927
- 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) {
13928
14152
  throw new Error(
13929
14153
  `${this.constructor.name} must implement the process method`
13930
14154
  )
@@ -13970,6 +14194,16 @@ class BlockMacroProcessor extends MacroProcessor {
13970
14194
  set name(value) {
13971
14195
  this._name = value;
13972
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
+ }
13973
14207
  }
13974
14208
  BlockMacroProcessor.DSL = MacroProcessorDsl;
13975
14209
 
@@ -14013,6 +14247,16 @@ class InlineMacroProcessor extends MacroProcessor {
14013
14247
  ))
14014
14248
  }
14015
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
+
14016
14260
  resolveRegexp(name, format) {
14017
14261
  if (!MacroNameRx.test(name)) {
14018
14262
  throw new Error(`invalid name for inline macro: ${name}`)
@@ -14376,6 +14620,15 @@ class Registry {
14376
14620
  return !!this._block_extensions
14377
14621
  }
14378
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
+
14379
14632
  /**
14380
14633
  * Check whether a BlockProcessor is registered for the given name and context.
14381
14634
  *
@@ -14422,6 +14675,17 @@ class Registry {
14422
14675
  return !!this._block_macro_extensions
14423
14676
  }
14424
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
+
14425
14689
  /**
14426
14690
  * Check whether a BlockMacroProcessor is registered for the given name.
14427
14691
  *
@@ -14527,6 +14791,85 @@ class Registry {
14527
14791
  return extension
14528
14792
  }
14529
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
+
14530
14873
  // ── Private helpers ──────────────────────────────────────────────────────────
14531
14874
 
14532
14875
  /** @internal */
@@ -14752,6 +15095,15 @@ const Extensions = {
14752
15095
  return _groups
14753
15096
  },
14754
15097
 
15098
+ /**
15099
+ * Alias for {@link groups}.
15100
+ *
15101
+ * @returns {object}
15102
+ */
15103
+ getGroups() {
15104
+ return this.groups()
15105
+ },
15106
+
14755
15107
  /**
14756
15108
  * Create a new Registry, optionally pre-populated with a named block.
14757
15109
  *
@@ -15979,8 +16331,9 @@ class Document extends AbstractBlock {
15979
16331
  * @param {Object} blockAttributes
15980
16332
  */
15981
16333
  playbackAttributes(blockAttributes) {
15982
- if (!('attribute_entries' in blockAttributes)) return
15983
- for (const entry of blockAttributes.attribute_entries) {
16334
+ const entries = getAttributeEntries(blockAttributes);
16335
+ if (!entries) return
16336
+ for (const entry of entries) {
15984
16337
  if (entry.negate) {
15985
16338
  delete this.attributes[entry.name];
15986
16339
  if (entry.name === 'compat-mode') this.compatMode = false;
@@ -16709,6 +17062,12 @@ class Document extends AbstractBlock {
16709
17062
  * Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
16710
17063
  */
16711
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
+ }
16712
17071
  // Skip title pre-computation for blocks with an explicit empty id ([id=]).
16713
17072
  // In Ruby, apply_title_subs is lazy: it is never called during parsing for such
16714
17073
  // blocks because section.title is never accessed. An explicit empty id is
@@ -16792,7 +17151,7 @@ class Document extends AbstractBlock {
16792
17151
  * @internal
16793
17152
  */
16794
17153
  _clearPlaybackAttributes(attributes) {
16795
- delete attributes.attribute_entries;
17154
+ delete attributes[ATTR_ENTRIES_KEY];
16796
17155
  }
16797
17156
 
16798
17157
  /**
@@ -19520,17 +19879,23 @@ async function load$1(input, options = {}) {
19520
19879
  // Shallow-copy options so we don't mutate the caller's object.
19521
19880
  options = Object.assign({}, options);
19522
19881
 
19523
- const timings = options.timings ?? null;
19524
- if (timings) timings.start('read');
19525
-
19526
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.
19527
19886
  if ('logger' in options) {
19528
- const logger = options.logger;
19529
- if (logger !== LoggerManager.logger) {
19530
- LoggerManager.logger = logger ?? new NullLogger();
19531
- }
19887
+ const newLogger = options.logger ?? new NullLogger();
19888
+ delete options.logger;
19889
+ return withLogger(newLogger, () => _doLoad(input, options, newLogger))
19532
19890
  }
19533
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
+
19534
19899
  // ── Attributes normalisation ──────────────────────────────────────────────
19535
19900
  let attrs = options.attributes;
19536
19901
  if (!attrs) {
@@ -19562,11 +19927,9 @@ async function load$1(input, options = {}) {
19562
19927
  attrs.docname = basename(inputPath, docfilesuffix);
19563
19928
  }
19564
19929
  source = await _readStream(input);
19565
- } else if (
19566
- typeof input === 'object' &&
19567
- input?.constructor?.name === 'Buffer'
19568
- ) {
19569
- 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);
19570
19933
  } else if (typeof input === 'string') {
19571
19934
  source = input;
19572
19935
  } else if (Array.isArray(input)) {
@@ -19629,6 +19992,19 @@ async function load$1(input, options = {}) {
19629
19992
  }
19630
19993
 
19631
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
+
19632
20008
  return doc
19633
20009
  }
19634
20010