@asciidoctor/core 4.0.0-alpha.2 → 4.0.0-alpha.4

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.
package/src/extensions.js CHANGED
@@ -27,6 +27,94 @@ import { basename } from './helpers.js'
27
27
  import { ATTR_REF_HEAD } from './constants.js'
28
28
  import { MacroNameRx, CC_ANY } from './rx.js'
29
29
 
30
+ // Type-only imports for JSDoc
31
+ /**
32
+ * @typedef {import('./document.js').Document} Document
33
+ * @typedef {import('./abstract_block.js').AbstractBlock} AbstractBlock
34
+ */
35
+
36
+ // ── DSL interface types ───────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * DSL interface for configuring a {@link Processor} instance.
40
+ * Applied to a processor instance via `Object.assign(instance, DslMixin)`.
41
+ *
42
+ * The `process` property behaves as a setter when called with a single Function
43
+ * argument (stores the process block), or as a passthrough caller otherwise.
44
+ *
45
+ * @typedef {object} ProcessorDslInterface
46
+ * @property {(key: string, value: unknown) => void} option - Set a config option.
47
+ * @property {(fn: (...args: unknown[]) => unknown) => void} process - Register the process function.
48
+ * @property {() => boolean} processBlockGiven - Returns true if a process function has been registered.
49
+ */
50
+
51
+ /**
52
+ * DSL interface for document processors (Preprocessor, TreeProcessor, Postprocessor, DocinfoProcessor).
53
+ *
54
+ * @typedef {ProcessorDslInterface & { prefer(): void; prepend(): void }} DocumentProcessorDslInterface
55
+ */
56
+
57
+ /**
58
+ * DSL interface for syntax processors (BlockProcessor, BlockMacroProcessor, InlineMacroProcessor).
59
+ *
60
+ * @typedef {ProcessorDslInterface & {
61
+ * named(value: string): void;
62
+ * contentModel(value: string): void;
63
+ * parseContentAs(value: string): void;
64
+ * positionalAttributes(...value: string[]): void;
65
+ * namePositionalAttributes(...value: string[]): void;
66
+ * positionalAttrs(...value: string[]): void;
67
+ * defaultAttributes(value: Record<string, string>): void;
68
+ * defaultAttrs(value: Record<string, string>): void;
69
+ * resolveAttributes(...args: unknown[]): void;
70
+ * resolvesAttributes(...args: unknown[]): void;
71
+ * }} SyntaxProcessorDslInterface
72
+ */
73
+
74
+ /**
75
+ * DSL interface for include processors.
76
+ *
77
+ * @typedef {DocumentProcessorDslInterface & {
78
+ * handles(fn: (doc: Document, target: string) => boolean): void;
79
+ * }} IncludeProcessorDslInterface
80
+ */
81
+
82
+ /**
83
+ * DSL interface for docinfo processors.
84
+ *
85
+ * @typedef {DocumentProcessorDslInterface & {
86
+ * atLocation(value: string): void;
87
+ * }} DocinfoProcessorDslInterface
88
+ */
89
+
90
+ /**
91
+ * DSL interface for block processors.
92
+ *
93
+ * @typedef {SyntaxProcessorDslInterface & {
94
+ * contexts(...value: (string | string[])[]): void;
95
+ * onContexts(...value: (string | string[])[]): void;
96
+ * onContext(...value: (string | string[])[]): void;
97
+ * bindTo(...value: (string | string[])[]): void;
98
+ * }} BlockProcessorDslInterface
99
+ */
100
+
101
+ /**
102
+ * DSL interface for macro processors (block and inline macros).
103
+ *
104
+ * @typedef {SyntaxProcessorDslInterface} MacroProcessorDslInterface
105
+ */
106
+
107
+ /**
108
+ * DSL interface for inline macro processors.
109
+ *
110
+ * @typedef {MacroProcessorDslInterface & {
111
+ * format(value: string): void;
112
+ * matchFormat(value: string): void;
113
+ * usingFormat(value: string): void;
114
+ * match(value: RegExp): void;
115
+ * }} InlineMacroProcessorDslInterface
116
+ */
117
+
30
118
  // ── DSL Mixins ────────────────────────────────────────────────────────────────
31
119
 
32
120
  /**
@@ -625,7 +713,12 @@ export class Processor {
625
713
  * Extensions.register(function () { this.preprocessor(CommentStripPreprocessor) })
626
714
  */
627
715
  export class Preprocessor extends Processor {
628
- process(_document, _reader) {
716
+ /**
717
+ * @param {Document} document - The document being parsed.
718
+ * @param {Reader} reader - The reader positioned at the beginning of the source.
719
+ * @returns {Reader|undefined} The same or a substitute Reader, or undefined to use the original.
720
+ */
721
+ process(document, reader) {
629
722
  throw new Error(
630
723
  `${this.constructor.name} must implement the process method`
631
724
  )
@@ -650,7 +743,11 @@ Preprocessor.DSL = DocumentProcessorDsl
650
743
  * Extensions.register(function () { this.treeProcessor(ShoutTreeProcessor) })
651
744
  */
652
745
  export class TreeProcessor extends Processor {
653
- process(_document) {
746
+ /**
747
+ * @param {Document} document - The parsed document.
748
+ * @returns {void}
749
+ */
750
+ process(document) {
654
751
  throw new Error(
655
752
  `${this.constructor.name} must implement the process method`
656
753
  )
@@ -679,7 +776,12 @@ export const Treeprocessor = TreeProcessor
679
776
  * Extensions.register(function () { this.postprocessor(FooterPostprocessor) })
680
777
  */
681
778
  export class Postprocessor extends Processor {
682
- process(_document, _output) {
779
+ /**
780
+ * @param {Document} document - The converted document.
781
+ * @param {string} output - The converted output string.
782
+ * @returns {string} The (possibly modified) output string.
783
+ */
784
+ process(document, output) {
683
785
  throw new Error(
684
786
  `${this.constructor.name} must implement the process method`
685
787
  )
@@ -693,13 +795,25 @@ Postprocessor.DSL = DocumentProcessorDsl
693
795
  * Implementations must extend IncludeProcessor.
694
796
  */
695
797
  export class IncludeProcessor extends Processor {
696
- process(_document, _reader, _target, _attributes) {
798
+ /**
799
+ * @param {Document} document - The document being parsed.
800
+ * @param {Reader} reader - The reader for the including document.
801
+ * @param {string} target - The target of the include directive.
802
+ * @param {Record<string, string>} attributes - The parsed include attributes.
803
+ * @returns {void}
804
+ */
805
+ process(document, reader, target, attributes) {
697
806
  throw new Error(
698
807
  `${this.constructor.name} must implement the process method`
699
808
  )
700
809
  }
701
810
 
702
- handles(_doc, _target) {
811
+ /**
812
+ * @param {Document} doc - The document being parsed.
813
+ * @param {string} target - The target of the include directive.
814
+ * @returns {boolean} true if this processor handles the given target.
815
+ */
816
+ handles(doc, target) {
703
817
  return true
704
818
  }
705
819
  }
@@ -717,7 +831,11 @@ export class DocinfoProcessor extends Processor {
717
831
  this.config.location ??= 'head'
718
832
  }
719
833
 
720
- process(_document) {
834
+ /**
835
+ * @param {Document} document - The document being converted.
836
+ * @returns {string} The docinfo content to inject into the document.
837
+ */
838
+ process(document) {
721
839
  throw new Error(
722
840
  `${this.constructor.name} must implement the process method`
723
841
  )
@@ -773,7 +891,13 @@ export class BlockProcessor extends Processor {
773
891
  this.config.content_model ??= 'compound'
774
892
  }
775
893
 
776
- process(_parent, _reader, _attributes) {
894
+ /**
895
+ * @param {AbstractBlock} parent - The enclosing block.
896
+ * @param {Reader} reader - The reader positioned at the block content.
897
+ * @param {Record<string, unknown>} attributes - The parsed block attributes.
898
+ * @returns {Block|void} A block node, or void to let the parser handle it.
899
+ */
900
+ process(parent, reader, attributes) {
777
901
  throw new Error(
778
902
  `${this.constructor.name} must implement the process method`
779
903
  )
@@ -791,7 +915,13 @@ export class MacroProcessor extends Processor {
791
915
  this.config.content_model ??= 'attributes'
792
916
  }
793
917
 
794
- process(_parent, _target, _attributes) {
918
+ /**
919
+ * @param {AbstractBlock} parent - The enclosing block.
920
+ * @param {string} target - The macro target (text between `name:` and `[`).
921
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
922
+ * @returns {Block|Inline|void}
923
+ */
924
+ process(parent, target, attributes) {
795
925
  throw new Error(
796
926
  `${this.constructor.name} must implement the process method`
797
927
  )
@@ -837,6 +967,16 @@ export class BlockMacroProcessor extends MacroProcessor {
837
967
  set name(value) {
838
968
  this._name = value
839
969
  }
970
+
971
+ /**
972
+ * @param {AbstractBlock} parent - The enclosing block.
973
+ * @param {string} target - The macro target.
974
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
975
+ * @returns {Block} A block node created with one of the `createBlock` helpers.
976
+ */
977
+ process(parent, target, attributes) {
978
+ return super.process(parent, target, attributes)
979
+ }
840
980
  }
841
981
  BlockMacroProcessor.DSL = MacroProcessorDsl
842
982
 
@@ -880,6 +1020,16 @@ export class InlineMacroProcessor extends MacroProcessor {
880
1020
  ))
881
1021
  }
882
1022
 
1023
+ /**
1024
+ * @param {AbstractBlock} parent - The enclosing block.
1025
+ * @param {string} target - The macro target.
1026
+ * @param {Record<string, unknown>} attributes - The parsed macro attributes.
1027
+ * @returns {Inline} An Inline node created with `this.createInline(...)`.
1028
+ */
1029
+ process(parent, target, attributes) {
1030
+ return super.process(parent, target, attributes)
1031
+ }
1032
+
883
1033
  resolveRegexp(name, format) {
884
1034
  if (!MacroNameRx.test(name)) {
885
1035
  throw new Error(`invalid name for inline macro: ${name}`)
@@ -1263,6 +1413,15 @@ export class Registry {
1263
1413
  return !!this._block_extensions
1264
1414
  }
1265
1415
 
1416
+ /**
1417
+ * Retrieve all BlockProcessor Extension proxy objects.
1418
+ *
1419
+ * @returns {ProcessorExtension[]}
1420
+ */
1421
+ blocks() {
1422
+ return this._block_extensions ? Object.values(this._block_extensions) : []
1423
+ }
1424
+
1266
1425
  /**
1267
1426
  * Check whether a BlockProcessor is registered for the given name and context.
1268
1427
  *
@@ -1309,6 +1468,17 @@ export class Registry {
1309
1468
  return !!this._block_macro_extensions
1310
1469
  }
1311
1470
 
1471
+ /**
1472
+ * Retrieve all BlockMacroProcessor Extension proxy objects.
1473
+ *
1474
+ * @returns {ProcessorExtension[]}
1475
+ */
1476
+ blockMacros() {
1477
+ return this._block_macro_extensions
1478
+ ? Object.values(this._block_macro_extensions)
1479
+ : []
1480
+ }
1481
+
1312
1482
  /**
1313
1483
  * Check whether a BlockMacroProcessor is registered for the given name.
1314
1484
  *
@@ -1414,6 +1584,85 @@ export class Registry {
1414
1584
  return extension
1415
1585
  }
1416
1586
 
1587
+ // ── JavaScript-style accessors ───────────────────────────────────────────────
1588
+
1589
+ /** @returns {object} the plain Object that maps names to groups for this registry. */
1590
+ getGroups() {
1591
+ return this.groups
1592
+ }
1593
+
1594
+ /** Alias for {@link preprocessors}. */
1595
+ getPreprocessors() {
1596
+ return this.preprocessors()
1597
+ }
1598
+
1599
+ /** Alias for {@link treeProcessors}. */
1600
+ getTreeProcessors() {
1601
+ return this.treeProcessors()
1602
+ }
1603
+
1604
+ /** Alias for {@link includeProcessors}. */
1605
+ getIncludeProcessors() {
1606
+ return this.includeProcessors()
1607
+ }
1608
+
1609
+ /** Alias for {@link postprocessors}. */
1610
+ getPostprocessors() {
1611
+ return this.postprocessors()
1612
+ }
1613
+
1614
+ /**
1615
+ * Alias for {@link docinfoProcessors}.
1616
+ *
1617
+ * @param {string|null} [location=null]
1618
+ */
1619
+ getDocinfoProcessors(location = null) {
1620
+ return this.docinfoProcessors(location)
1621
+ }
1622
+
1623
+ /** Alias for {@link blocks}. */
1624
+ getBlocks() {
1625
+ return this.blocks()
1626
+ }
1627
+
1628
+ /** Alias for {@link blockMacros}. */
1629
+ getBlockMacros() {
1630
+ return this.blockMacros()
1631
+ }
1632
+
1633
+ /** Alias for {@link inlineMacros}. */
1634
+ getInlineMacros() {
1635
+ return this.inlineMacros()
1636
+ }
1637
+
1638
+ /**
1639
+ * Alias for {@link registeredForInlineMacro}.
1640
+ *
1641
+ * @param {string} name
1642
+ */
1643
+ getInlineMacroFor(name) {
1644
+ return this.registeredForInlineMacro(name)
1645
+ }
1646
+
1647
+ /**
1648
+ * Alias for {@link registeredForBlock}.
1649
+ *
1650
+ * @param {string} name
1651
+ * @param {string} context
1652
+ */
1653
+ getBlockFor(name, context) {
1654
+ return this.registeredForBlock(name, context)
1655
+ }
1656
+
1657
+ /**
1658
+ * Alias for {@link registeredForBlockMacro}.
1659
+ *
1660
+ * @param {string} name
1661
+ */
1662
+ getBlockMacroFor(name) {
1663
+ return this.registeredForBlockMacro(name)
1664
+ }
1665
+
1417
1666
  // ── Private helpers ──────────────────────────────────────────────────────────
1418
1667
 
1419
1668
  /** @internal */
@@ -1639,6 +1888,15 @@ export const Extensions = {
1639
1888
  return _groups
1640
1889
  },
1641
1890
 
1891
+ /**
1892
+ * Alias for {@link groups}.
1893
+ *
1894
+ * @returns {object}
1895
+ */
1896
+ getGroups() {
1897
+ return this.groups()
1898
+ },
1899
+
1642
1900
  /**
1643
1901
  * Create a new Registry, optionally pre-populated with a named block.
1644
1902
  *
package/src/helpers.js CHANGED
@@ -38,7 +38,16 @@ const rstrip = (line) => line.replace(/[ \t\r\n\f\v]+$/, '')
38
38
  */
39
39
  export function prepareSourceArray(data, trimEnd = true) {
40
40
  if (!data.length) return []
41
- if (data[0].startsWith(BOM)) data[0] = data[0].slice(1)
41
+ if (data[0].startsWith(BOM)) {
42
+ data[0] = data[0].slice(1)
43
+ } else if (
44
+ data[0].charCodeAt(0) === 0xef &&
45
+ data[0].charCodeAt(1) === 0xbb &&
46
+ data[0].charCodeAt(2) === 0xbf
47
+ ) {
48
+ // Strip UTF-8 BOM encoded as three raw characters ( / \xEF\xBB\xBF) if not already decoded to U+FEFF
49
+ data[0] = data[0].slice(3)
50
+ }
42
51
  // Strip trailing \r to normalize Windows CRLF line endings (lines were split on \n, leaving \r).
43
52
  return trimEnd
44
53
  ? data.map(rstrip)
@@ -58,7 +67,16 @@ export function prepareSourceArray(data, trimEnd = true) {
58
67
  */
59
68
  export function prepareSourceString(data, trimEnd = true) {
60
69
  if (!data) return []
61
- if (data.startsWith(BOM)) data = data.slice(1)
70
+ if (data.startsWith(BOM)) {
71
+ data = data.slice(1)
72
+ } else if (
73
+ data.charCodeAt(0) === 0xef &&
74
+ data.charCodeAt(1) === 0xbb &&
75
+ data.charCodeAt(2) === 0xbf
76
+ ) {
77
+ // Strip UTF-8 BOM encoded as three raw characters ( / \xEF\xBB\xBF) if not already decoded to U+FEFF
78
+ data = data.slice(3)
79
+ }
62
80
  // Normalize Windows CRLF to LF so that split('\n') does not leave trailing \r on each line.
63
81
  if (data.includes('\r'))
64
82
  data = data.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
@@ -0,0 +1,139 @@
1
+ // HTTP cache system for URI fetching.
2
+ //
3
+ // Provides a pluggable caching layer for all HTTP(S) fetches performed during
4
+ // document conversion (includes, images, readContents). Mirrors the behaviour
5
+ // of Ruby's open-uri/cached mechanism activated by the `cache-uri` attribute.
6
+ //
7
+ // When `cache-uri` is set on the document:
8
+ // - If a cache has been registered via HttpCacheManager.setCache(), it is used.
9
+ // - Otherwise an ephemeral MemoryHttpCache is created for the duration of the
10
+ // conversion (keyed by Document instance via a WeakMap, GC'd with the doc).
11
+ //
12
+ // To implement a custom cache, extend HttpCache and override read(uri).
13
+
14
+ /**
15
+ * Base HTTP cache class.
16
+ *
17
+ * The default implementation delegates directly to fetch() with no caching.
18
+ * Subclasses override read() to add caching behaviour.
19
+ */
20
+ export class HttpCache {
21
+ /**
22
+ * Fetch content from a URI, optionally from a cache.
23
+ * @param {string} uri
24
+ * @returns {Promise<Response>}
25
+ */
26
+ async read(uri) {
27
+ return fetch(uri)
28
+ }
29
+ }
30
+
31
+ /**
32
+ * In-memory HTTP cache.
33
+ *
34
+ * Stores successful responses as ArrayBuffers keyed by URI. On a cache hit
35
+ * a synthetic Response is reconstructed from the stored data without touching
36
+ * the network. Non-OK responses (4xx, 5xx) are never cached.
37
+ *
38
+ * Safe as an ephemeral per-conversion cache or as a longer-lived process-level
39
+ * cache when registered via HttpCacheManager.setCache().
40
+ */
41
+ export class MemoryHttpCache extends HttpCache {
42
+ /** @type {Map<string, {buffer: ArrayBuffer, status: number, statusText: string, headers: Record<string,string>}>} */
43
+ #cache = new Map()
44
+
45
+ async read(uri) {
46
+ const entry = this.#cache.get(uri)
47
+ if (entry) {
48
+ return new Response(entry.buffer.slice(0), {
49
+ status: entry.status,
50
+ statusText: entry.statusText,
51
+ headers: entry.headers,
52
+ })
53
+ }
54
+ const response = await fetch(uri)
55
+ if (response.ok) {
56
+ const buffer = await response.arrayBuffer()
57
+ const headers = Object.fromEntries(response.headers.entries())
58
+ this.#cache.set(uri, {
59
+ buffer,
60
+ status: response.status,
61
+ statusText: response.statusText,
62
+ headers,
63
+ })
64
+ return new Response(buffer.slice(0), {
65
+ status: response.status,
66
+ statusText: response.statusText,
67
+ headers,
68
+ })
69
+ }
70
+ return response
71
+ }
72
+ }
73
+
74
+ /** @type {WeakMap<object, MemoryHttpCache>} */
75
+ const _ephemeralCaches = new WeakMap()
76
+
77
+ /**
78
+ * Singleton manager for the HTTP cache.
79
+ *
80
+ * Register a process-level cache:
81
+ * HttpCacheManager.setCache(new MemoryHttpCache())
82
+ * HttpCacheManager.setCache(new MyFileSystemCache('./cache'))
83
+ * HttpCacheManager.setCache(null) // revert to default ephemeral behaviour
84
+ *
85
+ * When no cache is registered and `cache-uri` is set, an ephemeral
86
+ * MemoryHttpCache is created per Document instance.
87
+ */
88
+ export const HttpCacheManager = {
89
+ /** @type {HttpCache|null} */
90
+ _cache: null,
91
+
92
+ /**
93
+ * Register a cache to use for all conversions.
94
+ * Pass null to unregister and revert to the ephemeral default.
95
+ * @param {HttpCache|null} cache
96
+ */
97
+ setCache(cache) {
98
+ this._cache = cache
99
+ },
100
+
101
+ /**
102
+ * Return the registered process-level cache, or null if none is registered.
103
+ * @returns {HttpCache|null}
104
+ */
105
+ getCache() {
106
+ return this._cache
107
+ },
108
+
109
+ /**
110
+ * Return the cache to use for a specific document conversion.
111
+ *
112
+ * Returns the registered cache if one exists; otherwise creates (or reuses)
113
+ * an ephemeral MemoryHttpCache scoped to the document's lifetime via a WeakMap.
114
+ * @param {object} doc - the current Document instance
115
+ * @returns {HttpCache}
116
+ */
117
+ getCacheForDocument(doc) {
118
+ if (this._cache) return this._cache
119
+ let cache = _ephemeralCaches.get(doc)
120
+ if (!cache) {
121
+ cache = new MemoryHttpCache()
122
+ _ephemeralCaches.set(doc, cache)
123
+ }
124
+ return cache
125
+ },
126
+ }
127
+
128
+ /**
129
+ * Fetch a URI, routing through the HTTP cache when `cache-uri` is set on the document.
130
+ * @param {string} uri
131
+ * @param {object} doc - the current Document instance
132
+ * @returns {Promise<Response>}
133
+ */
134
+ export function fetchUri(uri, doc) {
135
+ if (doc.hasAttribute('cache-uri')) {
136
+ return HttpCacheManager.getCacheForDocument(doc).read(uri)
137
+ }
138
+ return fetch(uri)
139
+ }
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  RevisionInfo,
11
11
  } from './document.js'
12
12
  import { Logger, LoggerManager, MemoryLogger, NullLogger } from './logging.js'
13
+ import { HttpCache, MemoryHttpCache, HttpCacheManager } from './http_cache.js'
13
14
  import { SafeMode, ContentModel } from './constants.js'
14
15
  import { Timings } from './timings.js'
15
16
  import { AbstractNode } from './abstract_node.js'
@@ -28,6 +29,18 @@ import {
28
29
  BlockMacroProcessor,
29
30
  Extensions,
30
31
  } from './extensions.js'
32
+
33
+ // Re-export DSL interface types for TypeScript consumers.
34
+ /**
35
+ * @typedef {import('./extensions.js').ProcessorDslInterface} ProcessorDslInterface
36
+ * @typedef {import('./extensions.js').DocumentProcessorDslInterface} DocumentProcessorDslInterface
37
+ * @typedef {import('./extensions.js').SyntaxProcessorDslInterface} SyntaxProcessorDslInterface
38
+ * @typedef {import('./extensions.js').IncludeProcessorDslInterface} IncludeProcessorDslInterface
39
+ * @typedef {import('./extensions.js').DocinfoProcessorDslInterface} DocinfoProcessorDslInterface
40
+ * @typedef {import('./extensions.js').BlockProcessorDslInterface} BlockProcessorDslInterface
41
+ * @typedef {import('./extensions.js').MacroProcessorDslInterface} MacroProcessorDslInterface
42
+ * @typedef {import('./extensions.js').InlineMacroProcessorDslInterface} InlineMacroProcessorDslInterface
43
+ */
31
44
  import {
32
45
  Converter,
33
46
  ConverterBase,
@@ -111,6 +124,9 @@ export {
111
124
  LoggerManager,
112
125
  MemoryLogger,
113
126
  NullLogger,
127
+ HttpCache,
128
+ MemoryHttpCache,
129
+ HttpCacheManager,
114
130
  SafeMode,
115
131
  ContentModel,
116
132
  Timings,
package/src/load.js CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { SpaceDelimiterRx, EscapedSpaceRx } from './rx.js'
19
19
  import { NULL, BACKEND_ALIASES } from './constants.js'
20
- import { LoggerManager, NullLogger } from './logging.js'
20
+ import { NullLogger, withLogger, getContextLogger } from './logging.js'
21
21
  import { basename, extname } from './helpers.js'
22
22
  import { Document } from './document.js'
23
23
  import { Converter } from './converter.js'
@@ -58,17 +58,23 @@ export async function load(input, options = {}) {
58
58
  // Shallow-copy options so we don't mutate the caller's object.
59
59
  options = Object.assign({}, options)
60
60
 
61
- const timings = options.timings ?? null
62
- if (timings) timings.start('read')
63
-
64
61
  // ── Logger override ───────────────────────────────────────────────────────
62
+ // When a logger option is supplied, run the conversion in an async-local
63
+ // context so the logger is scoped to this call only — no global mutation,
64
+ // which makes concurrent callers (e.g. parallel Deno tests) safe.
65
65
  if ('logger' in options) {
66
- const logger = options.logger
67
- if (logger !== LoggerManager.logger) {
68
- LoggerManager.logger = logger ?? new NullLogger()
69
- }
66
+ const newLogger = options.logger ?? new NullLogger()
67
+ delete options.logger
68
+ return withLogger(newLogger, () => _doLoad(input, options, newLogger))
70
69
  }
71
70
 
71
+ return _doLoad(input, options)
72
+ }
73
+
74
+ async function _doLoad(input, options, explicitLogger = null) {
75
+ const timings = options.timings ?? null
76
+ if (timings) timings.start('read')
77
+
72
78
  // ── Attributes normalisation ──────────────────────────────────────────────
73
79
  let attrs = options.attributes
74
80
  if (!attrs) {
@@ -100,11 +106,9 @@ export async function load(input, options = {}) {
100
106
  attrs.docname = basename(inputPath, docfilesuffix)
101
107
  }
102
108
  source = await _readStream(input)
103
- } else if (
104
- typeof input === 'object' &&
105
- input?.constructor?.name === 'Buffer'
106
- ) {
107
- source = input.toString('utf8')
109
+ } else if (input instanceof Uint8Array) {
110
+ // Covers both Node.js Buffer (a Uint8Array subclass) and browser Uint8Array shims.
111
+ source = new TextDecoder('utf-8').decode(input)
108
112
  } else if (typeof input === 'string') {
109
113
  source = input
110
114
  } else if (Array.isArray(input)) {
@@ -167,6 +171,19 @@ export async function load(input, options = {}) {
167
171
  }
168
172
 
169
173
  if (timings) timings.record('parse')
174
+
175
+ // Persist the logger on the Document instance so that doc.convert()
176
+ // (called by convert.js after the async-local context ends) still routes
177
+ // logging through the caller-supplied logger.
178
+ // ALS provides it in Node.js/Deno; the explicit parameter covers browser fallback.
179
+ const contextLogger = getContextLogger() ?? explicitLogger
180
+ if (contextLogger) {
181
+ Object.defineProperty(doc, 'logger', {
182
+ get: () => contextLogger,
183
+ configurable: true,
184
+ })
185
+ }
186
+
170
187
  return doc
171
188
  }
172
189