@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/build/browser/index.js +649 -131
- package/build/node/index.cjs +652 -131
- package/package.json +3 -2
- package/src/abstract_node.js +8 -13
- package/src/attribute_entry.js +17 -1
- package/src/browser/reader.js +2 -2
- package/src/browser.js +21 -0
- package/src/convert.js +10 -3
- package/src/converter/html5.js +8 -27
- package/src/converter/template.js +1 -1
- package/src/document.js +19 -9
- package/src/extensions.js +266 -8
- package/src/helpers.js +20 -2
- package/src/http_cache.js +139 -0
- package/src/index.js +16 -0
- package/src/load.js +30 -13
- package/src/logging.js +65 -5
- package/src/parser.js +1 -1
- package/src/path_resolver.js +20 -15
- package/src/reader.js +3 -2
- package/src/stylesheets.js +15 -0
- package/types/abstract_node.d.ts +1 -2
- package/types/attribute_entry.d.ts +8 -0
- package/types/browser/reader.d.ts +1 -1
- package/types/extensions.d.ts +203 -9
- package/types/http_cache.d.ts +59 -0
- package/types/index.d.cts +12 -1
- package/types/index.d.ts +12 -1
- package/types/logging.d.ts +15 -4
- package/types/path_resolver.d.ts +0 -5
- package/types/stylesheets.d.ts +1 -0
- package/types/table.d.ts +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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))
|
|
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 {
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|