@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.
@@ -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.2";
8
+ var version = "4.0.0-alpha.3";
9
9
  const packageJson = {
10
10
  version: version};
11
11
 
@@ -1514,6 +1514,65 @@ function resolveSeverity(severity) {
1514
1514
  return severity ?? Severity.UNKNOWN
1515
1515
  }
1516
1516
 
1517
+ // ── Per-execution logger context ─────────────────────────────────────────────
1518
+
1519
+ // Holds an AsyncLocalStorage instance once it is lazily initialised.
1520
+ // Allows per-execution logger isolation without mutating the global singleton,
1521
+ // making concurrent test execution (e.g. Deno's node:test) safe.
1522
+ let _loggerStore = null;
1523
+
1524
+ // Promise singleton — ensures AsyncLocalStorage is initialised at most once.
1525
+ let _loggerStorePromise = null;
1526
+
1527
+ /**
1528
+ * Lazily initialise an AsyncLocalStorage for per-execution logger context.
1529
+ * Falls back to null in environments that do not support node:async_hooks (e.g. browsers).
1530
+ * @returns {Promise<import('node:async_hooks').AsyncLocalStorage|null>}
1531
+ */
1532
+ async function _ensureLoggerStore() {
1533
+ if (_loggerStorePromise === null) {
1534
+ _loggerStorePromise = import('node:async_hooks')
1535
+ .then(({ AsyncLocalStorage }) => {
1536
+ const store = new AsyncLocalStorage();
1537
+ _loggerStore = store;
1538
+ return store
1539
+ })
1540
+ .catch(() => null);
1541
+ }
1542
+ return _loggerStorePromise
1543
+ }
1544
+
1545
+ /** @internal — returns the logger bound to the current async context, or null */
1546
+ function getContextLogger() {
1547
+ return _loggerStore?.getStore() ?? null
1548
+ }
1549
+
1550
+ /**
1551
+ * Run fn() within an async-local logger context so that all log calls via
1552
+ * `this.logger` (from applyLogging) automatically route to the provided logger
1553
+ * for the duration of the async execution chain.
1554
+ *
1555
+ * Falls back to global mutation in environments without node:async_hooks (e.g. browsers).
1556
+ *
1557
+ * @param {Logger|MemoryLogger|NullLogger} logger - The logger to activate.
1558
+ * @param {() => any} fn - The function to execute within the logger context.
1559
+ * @returns {Promise<any>}
1560
+ */
1561
+ async function withLogger(logger, fn) {
1562
+ const store = await _ensureLoggerStore();
1563
+ if (store) {
1564
+ return store.run(logger, fn)
1565
+ }
1566
+ // Fallback for environments without node:async_hooks (browsers).
1567
+ const prev = LoggerManager.logger;
1568
+ if (logger !== prev) LoggerManager.logger = logger;
1569
+ try {
1570
+ return await fn()
1571
+ } finally {
1572
+ if (logger !== prev) LoggerManager.logger = prev;
1573
+ }
1574
+ }
1575
+
1517
1576
  // ── Logger ────────────────────────────────────────────────────────────────────
1518
1577
 
1519
1578
  /** Standard logger that writes formatted messages to stderr or a custom pipe. */
@@ -1822,8 +1881,9 @@ class MemoryLogger {
1822
1881
  // ── NullLogger ────────────────────────────────────────────────────────────────
1823
1882
 
1824
1883
  /** Logger that discards all messages but still tracks the maximum severity. */
1825
- class NullLogger {
1884
+ class NullLogger extends Logger {
1826
1885
  constructor() {
1886
+ super();
1827
1887
  this.level = Severity.UNKNOWN;
1828
1888
  this._maxSeverity = null;
1829
1889
  }
@@ -1968,12 +2028,12 @@ const LoggerManager = (() => {
1968
2028
  function applyLogging(proto) {
1969
2029
  Object.defineProperty(proto, 'logger', {
1970
2030
  get() {
1971
- return LoggerManager.logger
2031
+ return _loggerStore?.getStore() ?? LoggerManager.logger
1972
2032
  },
1973
2033
  configurable: true,
1974
2034
  });
1975
2035
 
1976
- proto.getLogger = () => LoggerManager.logger;
2036
+ proto.getLogger = () => _loggerStore?.getStore() ?? LoggerManager.logger;
1977
2037
 
1978
2038
  proto.messageWithContext = (text, context = {}) =>
1979
2039
  Logger.AutoFormattingMessage.attach({ text, ...context });
@@ -2020,7 +2080,16 @@ const rstrip = (line) => line.replace(/[ \t\r\n\f\v]+$/, '');
2020
2080
  */
2021
2081
  function prepareSourceArray(data, trimEnd = true) {
2022
2082
  if (!data.length) return []
2023
- if (data[0].startsWith(BOM)) data[0] = data[0].slice(1);
2083
+ if (data[0].startsWith(BOM)) {
2084
+ data[0] = data[0].slice(1);
2085
+ } else if (
2086
+ data[0].charCodeAt(0) === 0xef &&
2087
+ data[0].charCodeAt(1) === 0xbb &&
2088
+ data[0].charCodeAt(2) === 0xbf
2089
+ ) {
2090
+ // Strip UTF-8 BOM encoded as three raw characters ( / \xEF\xBB\xBF) if not already decoded to U+FEFF
2091
+ data[0] = data[0].slice(3);
2092
+ }
2024
2093
  // Strip trailing \r to normalize Windows CRLF line endings (lines were split on \n, leaving \r).
2025
2094
  return trimEnd
2026
2095
  ? data.map(rstrip)
@@ -2040,7 +2109,16 @@ function prepareSourceArray(data, trimEnd = true) {
2040
2109
  */
2041
2110
  function prepareSourceString(data, trimEnd = true) {
2042
2111
  if (!data) return []
2043
- if (data.startsWith(BOM)) data = data.slice(1);
2112
+ if (data.startsWith(BOM)) {
2113
+ data = data.slice(1);
2114
+ } else if (
2115
+ data.charCodeAt(0) === 0xef &&
2116
+ data.charCodeAt(1) === 0xbb &&
2117
+ data.charCodeAt(2) === 0xbf
2118
+ ) {
2119
+ // Strip UTF-8 BOM encoded as three raw characters ( / \xEF\xBB\xBF) if not already decoded to U+FEFF
2120
+ data = data.slice(3);
2121
+ }
2044
2122
  // Normalize Windows CRLF to LF so that split('\n') does not leave trailing \r on each line.
2045
2123
  if (data.includes('\r'))
2046
2124
  data = data.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
@@ -5972,7 +6050,7 @@ const SyntaxHighlighter = new DefaultFactory();
5972
6050
  //
5973
6051
  // This logic is specific to Asciidoctor.js and has no equivalent in the upstream Ruby asciidoctor
5974
6052
  // implementation. It handles the case where the document is loaded in a browser environment
5975
- // (XMLHttpRequest / Fetch IO module) where paths can be file:// or http(s):// URIs.
6053
+ // where paths can be file:// or http(s):// URIs.
5976
6054
  //
5977
6055
  // The key behavioural differences from the standard file-system resolver:
5978
6056
  // - Relative targets are resolved by string concatenation against a URI context dir,
@@ -6021,7 +6099,7 @@ function _linkReplacement(reader, target, attrlist) {
6021
6099
  * 1. target starts with file:// → inc_path = relpath = target
6022
6100
  * 2. target is a URI → must descend from baseDir or allow-uri-read; else → link
6023
6101
  * 3. target is an absolute OS path → prepend file:// (or file:///)
6024
- * 4. baseDir == '.' → inc_path = relpath = target (resolved by XMLHttpRequest/fetch)
6102
+ * 4. baseDir == '.' → inc_path = relpath = target (resolved by fetch)
6025
6103
  * 5. baseDir starts with file:// OR baseDir is not a URI → inc_path = baseDir/target; relpath = target
6026
6104
  * 6. baseDir is an absolute URL → inc_path = baseDir/target; relpath = target
6027
6105
  *
@@ -6743,7 +6821,7 @@ class Reader {
6743
6821
  return this.source()
6744
6822
  }
6745
6823
  getLogger() {
6746
- return LoggerManager.logger
6824
+ return this._document?.logger ?? LoggerManager.logger
6747
6825
  }
6748
6826
  createLogMessage(text, context = {}) {
6749
6827
  return Logger.AutoFormattingMessage.attach({ text, ...context })
@@ -9568,6 +9646,22 @@ class AttributeList {
9568
9646
  }
9569
9647
  }
9570
9648
 
9649
+ // Symbol key used to store attribute entries on a block-attributes object without
9650
+ // polluting the public attributes (invisible to Object.keys / for-in / JSON.stringify).
9651
+ // Spread ({ ...attrs }) copies Symbol-keyed properties, so the entry survives the
9652
+ // shallow clone made in AbstractNode's constructor.
9653
+ const ATTR_ENTRIES_KEY = Symbol('attribute_entries');
9654
+
9655
+ /**
9656
+ * Return the attribute entries stored for the given block attributes object,
9657
+ * or undefined if none have been saved.
9658
+ * @param {Object} blockAttributes
9659
+ * @returns {AttributeEntry[]|undefined}
9660
+ */
9661
+ function getAttributeEntries(blockAttributes) {
9662
+ return blockAttributes[ATTR_ENTRIES_KEY]
9663
+ }
9664
+
9571
9665
  class AttributeEntry {
9572
9666
  constructor(name, value, negate = null) {
9573
9667
  this.name = name;
@@ -9576,7 +9670,7 @@ class AttributeEntry {
9576
9670
  }
9577
9671
 
9578
9672
  saveTo(blockAttributes) {
9579
- (blockAttributes.attribute_entries ??= []).push(this);
9673
+ (blockAttributes[ATTR_ENTRIES_KEY] ??= []).push(this);
9580
9674
  return this
9581
9675
  }
9582
9676
  }
@@ -11051,7 +11145,7 @@ class Parser {
11051
11145
  const Rdr = Reader;
11052
11146
  const result = await extension.processMethod(
11053
11147
  parent,
11054
- blockReader ?? new Rdr(lines),
11148
+ blockReader ?? new Rdr(lines, null, { document: parent.document }),
11055
11149
  { ...attributes }
11056
11150
  );
11057
11151
  if (!result || result === parent) return null
@@ -13173,6 +13267,94 @@ function _uniform(str, chr, len) {
13173
13267
  // references; they will be resolved when those modules implement the methods.
13174
13268
 
13175
13269
 
13270
+ // Type-only imports for JSDoc
13271
+ /**
13272
+ * @typedef {import('./document.js').Document} Document
13273
+ * @typedef {import('./abstract_block.js').AbstractBlock} AbstractBlock
13274
+ */
13275
+
13276
+ // ── DSL interface types ───────────────────────────────────────────────────────
13277
+
13278
+ /**
13279
+ * DSL interface for configuring a {@link Processor} instance.
13280
+ * Applied to a processor instance via `Object.assign(instance, DslMixin)`.
13281
+ *
13282
+ * The `process` property behaves as a setter when called with a single Function
13283
+ * argument (stores the process block), or as a passthrough caller otherwise.
13284
+ *
13285
+ * @typedef {object} ProcessorDslInterface
13286
+ * @property {(key: string, value: unknown) => void} option - Set a config option.
13287
+ * @property {(fn: (...args: unknown[]) => unknown) => void} process - Register the process function.
13288
+ * @property {() => boolean} processBlockGiven - Returns true if a process function has been registered.
13289
+ */
13290
+
13291
+ /**
13292
+ * DSL interface for document processors (Preprocessor, TreeProcessor, Postprocessor, DocinfoProcessor).
13293
+ *
13294
+ * @typedef {ProcessorDslInterface & { prefer(): void; prepend(): void }} DocumentProcessorDslInterface
13295
+ */
13296
+
13297
+ /**
13298
+ * DSL interface for syntax processors (BlockProcessor, BlockMacroProcessor, InlineMacroProcessor).
13299
+ *
13300
+ * @typedef {ProcessorDslInterface & {
13301
+ * named(value: string): void;
13302
+ * contentModel(value: string): void;
13303
+ * parseContentAs(value: string): void;
13304
+ * positionalAttributes(...value: string[]): void;
13305
+ * namePositionalAttributes(...value: string[]): void;
13306
+ * positionalAttrs(...value: string[]): void;
13307
+ * defaultAttributes(value: Record<string, string>): void;
13308
+ * defaultAttrs(value: Record<string, string>): void;
13309
+ * resolveAttributes(...args: unknown[]): void;
13310
+ * resolvesAttributes(...args: unknown[]): void;
13311
+ * }} SyntaxProcessorDslInterface
13312
+ */
13313
+
13314
+ /**
13315
+ * DSL interface for include processors.
13316
+ *
13317
+ * @typedef {DocumentProcessorDslInterface & {
13318
+ * handles(fn: (doc: Document, target: string) => boolean): void;
13319
+ * }} IncludeProcessorDslInterface
13320
+ */
13321
+
13322
+ /**
13323
+ * DSL interface for docinfo processors.
13324
+ *
13325
+ * @typedef {DocumentProcessorDslInterface & {
13326
+ * atLocation(value: string): void;
13327
+ * }} DocinfoProcessorDslInterface
13328
+ */
13329
+
13330
+ /**
13331
+ * DSL interface for block processors.
13332
+ *
13333
+ * @typedef {SyntaxProcessorDslInterface & {
13334
+ * contexts(...value: (string | string[])[]): void;
13335
+ * onContexts(...value: (string | string[])[]): void;
13336
+ * onContext(...value: (string | string[])[]): void;
13337
+ * bindTo(...value: (string | string[])[]): void;
13338
+ * }} BlockProcessorDslInterface
13339
+ */
13340
+
13341
+ /**
13342
+ * DSL interface for macro processors (block and inline macros).
13343
+ *
13344
+ * @typedef {SyntaxProcessorDslInterface} MacroProcessorDslInterface
13345
+ */
13346
+
13347
+ /**
13348
+ * DSL interface for inline macro processors.
13349
+ *
13350
+ * @typedef {MacroProcessorDslInterface & {
13351
+ * format(value: string): void;
13352
+ * matchFormat(value: string): void;
13353
+ * usingFormat(value: string): void;
13354
+ * match(value: RegExp): void;
13355
+ * }} InlineMacroProcessorDslInterface
13356
+ */
13357
+
13176
13358
  // ── DSL Mixins ────────────────────────────────────────────────────────────────
13177
13359
 
13178
13360
  /**
@@ -13768,7 +13950,12 @@ class Processor {
13768
13950
  * Extensions.register(function () { this.preprocessor(CommentStripPreprocessor) })
13769
13951
  */
13770
13952
  class Preprocessor extends Processor {
13771
- process(_document, _reader) {
13953
+ /**
13954
+ * @param {Document} document - The document being parsed.
13955
+ * @param {Reader} reader - The reader positioned at the beginning of the source.
13956
+ * @returns {Reader|undefined} The same or a substitute Reader, or undefined to use the original.
13957
+ */
13958
+ process(document, reader) {
13772
13959
  throw new Error(
13773
13960
  `${this.constructor.name} must implement the process method`
13774
13961
  )
@@ -13793,7 +13980,11 @@ Preprocessor.DSL = DocumentProcessorDsl;
13793
13980
  * Extensions.register(function () { this.treeProcessor(ShoutTreeProcessor) })
13794
13981
  */
13795
13982
  class TreeProcessor extends Processor {
13796
- process(_document) {
13983
+ /**
13984
+ * @param {Document} document - The parsed document.
13985
+ * @returns {void}
13986
+ */
13987
+ process(document) {
13797
13988
  throw new Error(
13798
13989
  `${this.constructor.name} must implement the process method`
13799
13990
  )
@@ -13819,7 +14010,12 @@ TreeProcessor.DSL = DocumentProcessorDsl;
13819
14010
  * Extensions.register(function () { this.postprocessor(FooterPostprocessor) })
13820
14011
  */
13821
14012
  class Postprocessor extends Processor {
13822
- process(_document, _output) {
14013
+ /**
14014
+ * @param {Document} document - The converted document.
14015
+ * @param {string} output - The converted output string.
14016
+ * @returns {string} The (possibly modified) output string.
14017
+ */
14018
+ process(document, output) {
13823
14019
  throw new Error(
13824
14020
  `${this.constructor.name} must implement the process method`
13825
14021
  )
@@ -13833,13 +14029,25 @@ Postprocessor.DSL = DocumentProcessorDsl;
13833
14029
  * Implementations must extend IncludeProcessor.
13834
14030
  */
13835
14031
  class IncludeProcessor extends Processor {
13836
- process(_document, _reader, _target, _attributes) {
14032
+ /**
14033
+ * @param {Document} document - The document being parsed.
14034
+ * @param {Reader} reader - The reader for the including document.
14035
+ * @param {string} target - The target of the include directive.
14036
+ * @param {Record<string, string>} attributes - The parsed include attributes.
14037
+ * @returns {void}
14038
+ */
14039
+ process(document, reader, target, attributes) {
13837
14040
  throw new Error(
13838
14041
  `${this.constructor.name} must implement the process method`
13839
14042
  )
13840
14043
  }
13841
14044
 
13842
- handles(_doc, _target) {
14045
+ /**
14046
+ * @param {Document} doc - The document being parsed.
14047
+ * @param {string} target - The target of the include directive.
14048
+ * @returns {boolean} true if this processor handles the given target.
14049
+ */
14050
+ handles(doc, target) {
13843
14051
  return true
13844
14052
  }
13845
14053
  }
@@ -13857,7 +14065,11 @@ class DocinfoProcessor extends Processor {
13857
14065
  this.config.location ??= 'head';
13858
14066
  }
13859
14067
 
13860
- process(_document) {
14068
+ /**
14069
+ * @param {Document} document - The document being converted.
14070
+ * @returns {string} The docinfo content to inject into the document.
14071
+ */
14072
+ process(document) {
13861
14073
  throw new Error(
13862
14074
  `${this.constructor.name} must implement the process method`
13863
14075
  )
@@ -13913,7 +14125,13 @@ class BlockProcessor extends Processor {
13913
14125
  this.config.content_model ??= 'compound';
13914
14126
  }
13915
14127
 
13916
- process(_parent, _reader, _attributes) {
14128
+ /**
14129
+ * @param {AbstractBlock} parent - The enclosing block.
14130
+ * @param {Reader} reader - The reader positioned at the block content.
14131
+ * @param {Record<string, unknown>} attributes - The parsed block attributes.
14132
+ * @returns {Block|void} A block node, or void to let the parser handle it.
14133
+ */
14134
+ process(parent, reader, attributes) {
13917
14135
  throw new Error(
13918
14136
  `${this.constructor.name} must implement the process method`
13919
14137
  )
@@ -13931,7 +14149,13 @@ class MacroProcessor extends Processor {
13931
14149
  this.config.content_model ??= 'attributes';
13932
14150
  }
13933
14151
 
13934
- process(_parent, _target, _attributes) {
14152
+ /**
14153
+ * @param {AbstractBlock} parent - The enclosing block.
14154
+ * @param {string} target - The macro target (text between `name:` and `[`).
14155
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
14156
+ * @returns {Block|Inline|void}
14157
+ */
14158
+ process(parent, target, attributes) {
13935
14159
  throw new Error(
13936
14160
  `${this.constructor.name} must implement the process method`
13937
14161
  )
@@ -13977,6 +14201,16 @@ class BlockMacroProcessor extends MacroProcessor {
13977
14201
  set name(value) {
13978
14202
  this._name = value;
13979
14203
  }
14204
+
14205
+ /**
14206
+ * @param {AbstractBlock} parent - The enclosing block.
14207
+ * @param {string} target - The macro target.
14208
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
14209
+ * @returns {Block} A block node created with one of the `createBlock` helpers.
14210
+ */
14211
+ process(parent, target, attributes) {
14212
+ return super.process(parent, target, attributes)
14213
+ }
13980
14214
  }
13981
14215
  BlockMacroProcessor.DSL = MacroProcessorDsl;
13982
14216
 
@@ -14020,6 +14254,16 @@ class InlineMacroProcessor extends MacroProcessor {
14020
14254
  ))
14021
14255
  }
14022
14256
 
14257
+ /**
14258
+ * @param {AbstractBlock} parent - The enclosing block.
14259
+ * @param {string} target - The macro target.
14260
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
14261
+ * @returns {Inline} An Inline node created with `this.createInline(...)`.
14262
+ */
14263
+ process(parent, target, attributes) {
14264
+ return super.process(parent, target, attributes)
14265
+ }
14266
+
14023
14267
  resolveRegexp(name, format) {
14024
14268
  if (!MacroNameRx.test(name)) {
14025
14269
  throw new Error(`invalid name for inline macro: ${name}`)
@@ -14383,6 +14627,15 @@ class Registry {
14383
14627
  return !!this._block_extensions
14384
14628
  }
14385
14629
 
14630
+ /**
14631
+ * Retrieve all BlockProcessor Extension proxy objects.
14632
+ *
14633
+ * @returns {ProcessorExtension[]}
14634
+ */
14635
+ blocks() {
14636
+ return this._block_extensions ? Object.values(this._block_extensions) : []
14637
+ }
14638
+
14386
14639
  /**
14387
14640
  * Check whether a BlockProcessor is registered for the given name and context.
14388
14641
  *
@@ -14429,6 +14682,17 @@ class Registry {
14429
14682
  return !!this._block_macro_extensions
14430
14683
  }
14431
14684
 
14685
+ /**
14686
+ * Retrieve all BlockMacroProcessor Extension proxy objects.
14687
+ *
14688
+ * @returns {ProcessorExtension[]}
14689
+ */
14690
+ blockMacros() {
14691
+ return this._block_macro_extensions
14692
+ ? Object.values(this._block_macro_extensions)
14693
+ : []
14694
+ }
14695
+
14432
14696
  /**
14433
14697
  * Check whether a BlockMacroProcessor is registered for the given name.
14434
14698
  *
@@ -14534,6 +14798,85 @@ class Registry {
14534
14798
  return extension
14535
14799
  }
14536
14800
 
14801
+ // ── JavaScript-style accessors ───────────────────────────────────────────────
14802
+
14803
+ /** @returns {object} the plain Object that maps names to groups for this registry. */
14804
+ getGroups() {
14805
+ return this.groups
14806
+ }
14807
+
14808
+ /** Alias for {@link preprocessors}. */
14809
+ getPreprocessors() {
14810
+ return this.preprocessors()
14811
+ }
14812
+
14813
+ /** Alias for {@link treeProcessors}. */
14814
+ getTreeProcessors() {
14815
+ return this.treeProcessors()
14816
+ }
14817
+
14818
+ /** Alias for {@link includeProcessors}. */
14819
+ getIncludeProcessors() {
14820
+ return this.includeProcessors()
14821
+ }
14822
+
14823
+ /** Alias for {@link postprocessors}. */
14824
+ getPostprocessors() {
14825
+ return this.postprocessors()
14826
+ }
14827
+
14828
+ /**
14829
+ * Alias for {@link docinfoProcessors}.
14830
+ *
14831
+ * @param {string|null} [location=null]
14832
+ */
14833
+ getDocinfoProcessors(location = null) {
14834
+ return this.docinfoProcessors(location)
14835
+ }
14836
+
14837
+ /** Alias for {@link blocks}. */
14838
+ getBlocks() {
14839
+ return this.blocks()
14840
+ }
14841
+
14842
+ /** Alias for {@link blockMacros}. */
14843
+ getBlockMacros() {
14844
+ return this.blockMacros()
14845
+ }
14846
+
14847
+ /** Alias for {@link inlineMacros}. */
14848
+ getInlineMacros() {
14849
+ return this.inlineMacros()
14850
+ }
14851
+
14852
+ /**
14853
+ * Alias for {@link registeredForInlineMacro}.
14854
+ *
14855
+ * @param {string} name
14856
+ */
14857
+ getInlineMacroFor(name) {
14858
+ return this.registeredForInlineMacro(name)
14859
+ }
14860
+
14861
+ /**
14862
+ * Alias for {@link registeredForBlock}.
14863
+ *
14864
+ * @param {string} name
14865
+ * @param {string} context
14866
+ */
14867
+ getBlockFor(name, context) {
14868
+ return this.registeredForBlock(name, context)
14869
+ }
14870
+
14871
+ /**
14872
+ * Alias for {@link registeredForBlockMacro}.
14873
+ *
14874
+ * @param {string} name
14875
+ */
14876
+ getBlockMacroFor(name) {
14877
+ return this.registeredForBlockMacro(name)
14878
+ }
14879
+
14537
14880
  // ── Private helpers ──────────────────────────────────────────────────────────
14538
14881
 
14539
14882
  /** @internal */
@@ -14759,6 +15102,15 @@ const Extensions = {
14759
15102
  return _groups
14760
15103
  },
14761
15104
 
15105
+ /**
15106
+ * Alias for {@link groups}.
15107
+ *
15108
+ * @returns {object}
15109
+ */
15110
+ getGroups() {
15111
+ return this.groups()
15112
+ },
15113
+
14762
15114
  /**
14763
15115
  * Create a new Registry, optionally pre-populated with a named block.
14764
15116
  *
@@ -15986,8 +16338,9 @@ class Document extends AbstractBlock {
15986
16338
  * @param {Object} blockAttributes
15987
16339
  */
15988
16340
  playbackAttributes(blockAttributes) {
15989
- if (!('attribute_entries' in blockAttributes)) return
15990
- for (const entry of blockAttributes.attribute_entries) {
16341
+ const entries = getAttributeEntries(blockAttributes);
16342
+ if (!entries) return
16343
+ for (const entry of entries) {
15991
16344
  if (entry.negate) {
15992
16345
  delete this.attributes[entry.name];
15993
16346
  if (entry.name === 'compat-mode') this.compatMode = false;
@@ -16716,6 +17069,12 @@ class Document extends AbstractBlock {
16716
17069
  * Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
16717
17070
  */
16718
17071
  async _resolveAllTexts(block) {
17072
+ // The header section lives outside document.blocks; pre-compute its title here so
17073
+ // that doc.doctitle() returns the fully-substituted title (with replacements applied,
17074
+ // e.g. ' → &#8217;) rather than the header-subs-only fallback.
17075
+ if (block === this && this.header) {
17076
+ await this.header.precomputeTitle?.();
17077
+ }
16719
17078
  // Skip title pre-computation for blocks with an explicit empty id ([id=]).
16720
17079
  // In Ruby, apply_title_subs is lazy: it is never called during parsing for such
16721
17080
  // blocks because section.title is never accessed. An explicit empty id is
@@ -16799,7 +17158,7 @@ class Document extends AbstractBlock {
16799
17158
  * @internal
16800
17159
  */
16801
17160
  _clearPlaybackAttributes(attributes) {
16802
- delete attributes.attribute_entries;
17161
+ delete attributes[ATTR_ENTRIES_KEY];
16803
17162
  }
16804
17163
 
16805
17164
  /**
@@ -19527,17 +19886,23 @@ async function load$1(input, options = {}) {
19527
19886
  // Shallow-copy options so we don't mutate the caller's object.
19528
19887
  options = Object.assign({}, options);
19529
19888
 
19530
- const timings = options.timings ?? null;
19531
- if (timings) timings.start('read');
19532
-
19533
19889
  // ── Logger override ───────────────────────────────────────────────────────
19890
+ // When a logger option is supplied, run the conversion in an async-local
19891
+ // context so the logger is scoped to this call only — no global mutation,
19892
+ // which makes concurrent callers (e.g. parallel Deno tests) safe.
19534
19893
  if ('logger' in options) {
19535
- const logger = options.logger;
19536
- if (logger !== LoggerManager.logger) {
19537
- LoggerManager.logger = logger ?? new NullLogger();
19538
- }
19894
+ const newLogger = options.logger ?? new NullLogger();
19895
+ delete options.logger;
19896
+ return withLogger(newLogger, () => _doLoad(input, options, newLogger))
19539
19897
  }
19540
19898
 
19899
+ return _doLoad(input, options)
19900
+ }
19901
+
19902
+ async function _doLoad(input, options, explicitLogger = null) {
19903
+ const timings = options.timings ?? null;
19904
+ if (timings) timings.start('read');
19905
+
19541
19906
  // ── Attributes normalisation ──────────────────────────────────────────────
19542
19907
  let attrs = options.attributes;
19543
19908
  if (!attrs) {
@@ -19569,11 +19934,9 @@ async function load$1(input, options = {}) {
19569
19934
  attrs.docname = basename(inputPath, docfilesuffix);
19570
19935
  }
19571
19936
  source = await _readStream(input);
19572
- } else if (
19573
- typeof input === 'object' &&
19574
- input?.constructor?.name === 'Buffer'
19575
- ) {
19576
- source = input.toString('utf8');
19937
+ } else if (input instanceof Uint8Array) {
19938
+ // Covers both Node.js Buffer (a Uint8Array subclass) and browser Uint8Array shims.
19939
+ source = new TextDecoder('utf-8').decode(input);
19577
19940
  } else if (typeof input === 'string') {
19578
19941
  source = input;
19579
19942
  } else if (Array.isArray(input)) {
@@ -19636,6 +19999,19 @@ async function load$1(input, options = {}) {
19636
19999
  }
19637
20000
 
19638
20001
  if (timings) timings.record('parse');
20002
+
20003
+ // Persist the logger on the Document instance so that doc.convert()
20004
+ // (called by convert.js after the async-local context ends) still routes
20005
+ // logging through the caller-supplied logger.
20006
+ // ALS provides it in Node.js/Deno; the explicit parameter covers browser fallback.
20007
+ const contextLogger = getContextLogger() ?? explicitLogger;
20008
+ if (contextLogger) {
20009
+ Object.defineProperty(doc, 'logger', {
20010
+ get: () => contextLogger,
20011
+ configurable: true,
20012
+ });
20013
+ }
20014
+
19639
20015
  return doc
19640
20016
  }
19641
20017
 
@@ -22534,7 +22910,7 @@ class TemplateConverter extends ConverterBase {
22534
22910
 
22535
22911
  let entries;
22536
22912
  try {
22537
- entries = await node_fs.promises.readdir(templateDir);
22913
+ entries = (await node_fs.promises.readdir(templateDir)).sort();
22538
22914
  } catch {
22539
22915
  return result
22540
22916
  }