@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asciidoctor/core",
3
- "version": "4.0.0-alpha.1",
3
+ "version": "4.0.0-alpha.3",
4
4
  "description": "Asciidoctor.js: AsciiDoc in JavaScript powered by Asciidoctor",
5
5
  "type": "module",
6
6
  "main": "build/node/index.cjs",
@@ -9,9 +9,14 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "browser": "./build/browser/index.js",
12
- "require": "./build/node/index.cjs",
13
- "import": "./src/index.js",
14
- "types": "./types/index.d.ts"
12
+ "require": {
13
+ "types": "./types/index.d.cts",
14
+ "default": "./build/node/index.cjs"
15
+ },
16
+ "import": {
17
+ "types": "./types/index.d.ts",
18
+ "default": "./src/index.js"
19
+ }
15
20
  }
16
21
  },
17
22
  "engines": {
@@ -23,6 +28,7 @@
23
28
  "build:types": "tsc && node scripts/strip-internal.js && node scripts/patch-jsdoc.js",
24
29
  "docs": "typedoc --tsconfig tsconfig.docs.json",
25
30
  "test": "node --test test/**.test.js",
31
+ "test:deno": "deno test --allow-env --no-check --allow-read --allow-sys --allow-write --allow-run --allow-net test/*.test.js",
26
32
  "test:coverage": "node --test --experimental-test-coverage --test-coverage-include='src/**' test/**.test.js",
27
33
  "test:browser": "vitest run --config vitest.browser.config.js",
28
34
  "lint": "biome lint src test",
@@ -60,7 +66,7 @@
60
66
  },
61
67
  "homepage": "https://github.com/asciidoctor/asciidoctor.js",
62
68
  "volta": {
63
- "node": "24.14.1"
69
+ "node": "24.15.0"
64
70
  },
65
71
  "devDependencies": {
66
72
  "@biomejs/biome": "^2.4.13",
@@ -502,7 +502,7 @@ export class AbstractBlock extends AbstractNode {
502
502
  * - `'reject'` → skip the node and its children
503
503
  * - `'stop'` → include the node (if it matched) and stop the entire traversal
504
504
  *
505
- * @param {Object} [selector={}] - Selector criteria object.
505
+ * @param {Object|Function} [selector={}] - Selector criteria object, or a filter callback when called as `findBy(callback)`.
506
506
  * @param {Function|null} [filter=null] - Per-node filter callback.
507
507
  * @returns {AbstractBlock[]} array of matching block-level nodes.
508
508
  *
@@ -517,6 +517,9 @@ export class AbstractBlock extends AbstractNode {
517
517
  *
518
518
  * @example <caption>All image blocks including those inside AsciiDoc table cells</caption>
519
519
  * const images = doc.findBy({ context: 'image', traverseDocuments: true })
520
+ *
521
+ * @example <caption>Filter-only shorthand (no selector)</caption>
522
+ * const verbatim = doc.findBy((b) => b.contentModel === ContentModel.VERBATIM)
520
523
  */
521
524
  findBy(selector = {}, filter = null) {
522
525
  const result = []
@@ -525,8 +528,13 @@ export class AbstractBlock extends AbstractNode {
525
528
  selector && typeof selector === 'object' && !Array.isArray(selector)
526
529
  ? selector
527
530
  : {}
528
- // Normalise: filter must be a function; ignore string/non-function values (mirrors core API).
529
- const normFilter = typeof filter === 'function' ? filter : null
531
+ // Normalise: support findBy(callback) shorthand selector is the filter when it's a function.
532
+ const normFilter =
533
+ typeof filter === 'function'
534
+ ? filter
535
+ : typeof selector === 'function'
536
+ ? selector
537
+ : null
530
538
  try {
531
539
  this.#findByInternal(normSelector, result, normFilter)
532
540
  } catch (e) {
@@ -1,3 +1,19 @@
1
+ // Symbol key used to store attribute entries on a block-attributes object without
2
+ // polluting the public attributes (invisible to Object.keys / for-in / JSON.stringify).
3
+ // Spread ({ ...attrs }) copies Symbol-keyed properties, so the entry survives the
4
+ // shallow clone made in AbstractNode's constructor.
5
+ export const ATTR_ENTRIES_KEY = Symbol('attribute_entries')
6
+
7
+ /**
8
+ * Return the attribute entries stored for the given block attributes object,
9
+ * or undefined if none have been saved.
10
+ * @param {Object} blockAttributes
11
+ * @returns {AttributeEntry[]|undefined}
12
+ */
13
+ export function getAttributeEntries(blockAttributes) {
14
+ return blockAttributes[ATTR_ENTRIES_KEY]
15
+ }
16
+
1
17
  export class AttributeEntry {
2
18
  constructor(name, value, negate = null) {
3
19
  this.name = name
@@ -6,7 +22,7 @@ export class AttributeEntry {
6
22
  }
7
23
 
8
24
  saveTo(blockAttributes) {
9
- ;(blockAttributes.attribute_entries ??= []).push(this)
25
+ ;(blockAttributes[ATTR_ENTRIES_KEY] ??= []).push(this)
10
26
  return this
11
27
  }
12
28
  }
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // This logic is specific to Asciidoctor.js and has no equivalent in the upstream Ruby asciidoctor
7
7
  // implementation. It handles the case where the document is loaded in a browser environment
8
- // (XMLHttpRequest / Fetch IO module) where paths can be file:// or http(s):// URIs.
8
+ // where paths can be file:// or http(s):// URIs.
9
9
  //
10
10
  // The key behavioural differences from the standard file-system resolver:
11
11
  // - Relative targets are resolved by string concatenation against a URI context dir,
@@ -55,7 +55,7 @@ function _linkReplacement(reader, target, attrlist) {
55
55
  * 1. target starts with file:// → inc_path = relpath = target
56
56
  * 2. target is a URI → must descend from baseDir or allow-uri-read; else → link
57
57
  * 3. target is an absolute OS path → prepend file:// (or file:///)
58
- * 4. baseDir == '.' → inc_path = relpath = target (resolved by XMLHttpRequest/fetch)
58
+ * 4. baseDir == '.' → inc_path = relpath = target (resolved by fetch)
59
59
  * 5. baseDir starts with file:// OR baseDir is not a URI → inc_path = baseDir/target; relpath = target
60
60
  * 6. baseDir is an absolute URL → inc_path = baseDir/target; relpath = target
61
61
  *
@@ -840,10 +840,7 @@ ${await node.content()}
840
840
  img =
841
841
  (await this.readSvgContents(node, target)) ||
842
842
  `<span class="alt">${node.getAlt()}</span>`
843
- } else if (
844
- node.hasOption('interactive') &&
845
- node.document.safe >= SafeMode.SERVER
846
- ) {
843
+ } else if (node.hasOption('interactive')) {
847
844
  const fallback = node.hasAttribute('fallback')
848
845
  ? `<img src="${await node.imageUri(node.getAttribute('fallback'))}" alt="${this._encodeAttrValue(node.getAlt())}"${widthAttr}${heightAttr}${slash}>`
849
846
  : `<span class="alt">${node.getAlt()}</span>`
@@ -1684,10 +1681,7 @@ Your browser does not support the video tag.
1684
1681
  img =
1685
1682
  (await this.readSvgContents(node, target)) ||
1686
1683
  `<span class="alt">${node.getAlt()}</span>`
1687
- } else if (
1688
- node.hasOption('interactive') &&
1689
- node.document.safe >= SafeMode.SERVER
1690
- ) {
1684
+ } else if (node.hasOption('interactive')) {
1691
1685
  const fallback = node.hasAttribute('fallback')
1692
1686
  ? `<img src="${await node.imageUri(node.getAttribute('fallback'))}" alt="${this._encodeAttrValue(node.getAlt())}"${attrs}${this._voidSlash}>`
1693
1687
  : `<span class="alt">${node.getAlt()}</span>`
@@ -276,7 +276,7 @@ export class TemplateConverter extends ConverterBase {
276
276
 
277
277
  let entries
278
278
  try {
279
- entries = await fsp.readdir(templateDir)
279
+ entries = (await fsp.readdir(templateDir)).sort()
280
280
  } catch {
281
281
  return result
282
282
  }
package/src/document.js CHANGED
@@ -68,7 +68,11 @@ export class ImageReference {
68
68
  import { Footnote } from './footnote.js'
69
69
  export { Footnote }
70
70
 
71
- import { AttributeEntry } from './attribute_entry.js'
71
+ import {
72
+ AttributeEntry,
73
+ getAttributeEntries,
74
+ ATTR_ENTRIES_KEY,
75
+ } from './attribute_entry.js'
72
76
  export { AttributeEntry }
73
77
 
74
78
  /**
@@ -927,8 +931,9 @@ export class Document extends AbstractBlock {
927
931
  * @param {Object} blockAttributes
928
932
  */
929
933
  playbackAttributes(blockAttributes) {
930
- if (!('attribute_entries' in blockAttributes)) return
931
- for (const entry of blockAttributes.attribute_entries) {
934
+ const entries = getAttributeEntries(blockAttributes)
935
+ if (!entries) return
936
+ for (const entry of entries) {
932
937
  if (entry.negate) {
933
938
  delete this.attributes[entry.name]
934
939
  if (entry.name === 'compat-mode') this.compatMode = false
@@ -1657,6 +1662,12 @@ export class Document extends AbstractBlock {
1657
1662
  * Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
1658
1663
  */
1659
1664
  async _resolveAllTexts(block) {
1665
+ // The header section lives outside document.blocks; pre-compute its title here so
1666
+ // that doc.doctitle() returns the fully-substituted title (with replacements applied,
1667
+ // e.g. ' → &#8217;) rather than the header-subs-only fallback.
1668
+ if (block === this && this.header) {
1669
+ await this.header.precomputeTitle?.()
1670
+ }
1660
1671
  // Skip title pre-computation for blocks with an explicit empty id ([id=]).
1661
1672
  // In Ruby, apply_title_subs is lazy: it is never called during parsing for such
1662
1673
  // blocks because section.title is never accessed. An explicit empty id is
@@ -1740,7 +1751,7 @@ export class Document extends AbstractBlock {
1740
1751
  * @internal
1741
1752
  */
1742
1753
  _clearPlaybackAttributes(attributes) {
1743
- delete attributes.attribute_entries
1754
+ delete attributes[ATTR_ENTRIES_KEY]
1744
1755
  }
1745
1756
 
1746
1757
  /**
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')
package/src/index.js CHANGED
@@ -28,6 +28,18 @@ import {
28
28
  BlockMacroProcessor,
29
29
  Extensions,
30
30
  } from './extensions.js'
31
+
32
+ // Re-export DSL interface types for TypeScript consumers.
33
+ /**
34
+ * @typedef {import('./extensions.js').ProcessorDslInterface} ProcessorDslInterface
35
+ * @typedef {import('./extensions.js').DocumentProcessorDslInterface} DocumentProcessorDslInterface
36
+ * @typedef {import('./extensions.js').SyntaxProcessorDslInterface} SyntaxProcessorDslInterface
37
+ * @typedef {import('./extensions.js').IncludeProcessorDslInterface} IncludeProcessorDslInterface
38
+ * @typedef {import('./extensions.js').DocinfoProcessorDslInterface} DocinfoProcessorDslInterface
39
+ * @typedef {import('./extensions.js').BlockProcessorDslInterface} BlockProcessorDslInterface
40
+ * @typedef {import('./extensions.js').MacroProcessorDslInterface} MacroProcessorDslInterface
41
+ * @typedef {import('./extensions.js').InlineMacroProcessorDslInterface} InlineMacroProcessorDslInterface
42
+ */
31
43
  import {
32
44
  Converter,
33
45
  ConverterBase,