@ai-react-markdown/engine 2.4.3 → 2.4.5
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/README.md +95 -0
- package/dist/index.cjs +116 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.dev.cjs +116 -33
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +116 -33
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +116 -33
- package/dist/index.js.map +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @ai-react-markdown/engine
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@ai-react-markdown/engine)
|
|
4
|
+
[](https://www.npmjs.com/package/@ai-react-markdown/engine)
|
|
5
|
+
[](https://bundlephobia.com/package/@ai-react-markdown/engine)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](#installation)
|
|
10
|
+
[](https://github.com/AIEPhoenix/ai-react-markdown/blob/main/LICENSE)
|
|
11
|
+
|
|
12
|
+
[](https://github.com/AIEPhoenix/ai-react-markdown/actions/workflows/ci.yml)
|
|
13
|
+
[](https://github.com/AIEPhoenix/ai-react-markdown/actions/workflows/release.yml)
|
|
14
|
+
[](https://github.com/AIEPhoenix/ai-react-markdown)
|
|
15
|
+
|
|
3
16
|
Framework-agnostic Markdown engine for [ai-react-markdown](https://github.com/AIEPhoenix/ai-react-markdown) — incremental parsing, LaTeX preprocessing, definition/footnote machinery, and the unified plugin pipeline. Takes Markdown text in, produces a [hast](https://github.com/syntax-tree/hast) tree plus incremental-parse state out; rendering that tree is the job of a framework adapter such as [`@ai-react-markdown/core`](https://www.npmjs.com/package/@ai-react-markdown/core) (React).
|
|
4
17
|
|
|
5
18
|
> **Status: internal supplier.** This package exists to serve
|
|
@@ -10,12 +23,94 @@ Framework-agnostic Markdown engine for [ai-react-markdown](https://github.com/AI
|
|
|
10
23
|
> instead; this package is interesting to you only if you are building a
|
|
11
24
|
> framework adapter of your own.
|
|
12
25
|
|
|
26
|
+
## What's inside
|
|
27
|
+
|
|
28
|
+
Everything is exported from the package root (`import { … } from '@ai-react-markdown/engine'`); the barrel is grouped by layer:
|
|
29
|
+
|
|
30
|
+
| Layer | Modules | Highlights |
|
|
31
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| Preprocessors | `preprocessors/latex`, `preprocessors/remend`, `preprocessAIMDContent` | `preprocessLaTeX(text)` (currency `$`, `\[…\]` / `\(…\)` normalization, code-fence and inline-code protection), `createIncrementalLatexPreprocessor()` for append-only streams, `remend` for unterminated-markup mending |
|
|
33
|
+
| Incremental parsing | `incrementalParse/*` | `advanceIncrementalParse(state, content, options)` — the prefix-freeze engine: a line scanner decides a verified-safe freeze boundary, only the tail re-parses, and the two trees are spliced; every frame is deep-equal to a full parse (enforced by the arbiter suites) or falls back to one |
|
|
34
|
+
| Pipeline assembly | `markdown/*`, `pluginChain`, `plugins/catalog`, `customMdastHandlers`, `remarkInjectPhantomDefs`, `rehypeRebaseHashLinks`, `rehypeFooterAdorn` | `buildCoreRemarkPlugins` / `buildCoreRehypePlugins` / `buildCoreRemarkRehypeOptions` — the exact chains the React renderer uses; the sealed engine-plugin catalog (`highlight`, `definitionList`, `removeComments`, `smartypants`, `pangu`, `defaultEnginePlugins`) |
|
|
35
|
+
| Cross-chunk coordination | `documentRegistry`, `collectDefLabels`, `extractContributions`, `extractDefBodiesFromHast`, `crossChunkUrlSanitize` | `createRegistry()` — the per-document store that numbers footnotes and resolves link definitions across chunks; `sanitizeCrossChunkUrl()` mirrors the standalone two-gate URL policy |
|
|
36
|
+
| Sanitization | `sanitizeSchema`, `extendSanitizeSchema`, `markdown/urlTransform` | The library default `rehype-sanitize` schema (read-only singleton — clone with `extendSanitizeSchema`), `defaultUrlTransform` |
|
|
37
|
+
| Streaming | `smoothStream/controller` | `createSmoothStreamController()` — the framework-agnostic typewriter pacing state machine behind `<AIMarkdownSmoothStream>`, with `SMOOTH_STREAM_PACING_PRESETS` |
|
|
38
|
+
| Leaves | `hastPredicates`, `normalizeId`, `shortenDocumentId`, `devStageTimings`, `fixtures/scenarios` | Small pure helpers and the shared test corpus |
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @ai-react-markdown/engine
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Dual ESM/CJS build with types for both. No React dependency. The only peer is `katex` (`^0.16 || ^0.17`, **optional** — needed only if you render math). It ships transitively via `rehype-katex`, so hoisted installers resolve it automatically; strict-isolation installers (yarn PnP, `pnpm --node-linker=isolated`) must install it explicitly in your app.
|
|
47
|
+
|
|
48
|
+
## Example: the LaTeX preprocessor on its own
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { preprocessLaTeX } from '@ai-react-markdown/engine';
|
|
52
|
+
|
|
53
|
+
preprocessLaTeX('Price is $100, and \\(x^2\\) is inline math.');
|
|
54
|
+
// → 'Price is \\$100, and $$x^2$$ is inline math.'
|
|
55
|
+
// (currency `$` escaped; `\\(…\\)` normalized to the `$$…$$` form remark-math's inline rule accepts)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The same function runs inside `@ai-react-markdown/core` before every parse; the incremental variant (`createIncrementalLatexPreprocessor`) reuses work across append-only frames.
|
|
59
|
+
|
|
60
|
+
## Example: driving the incremental parser
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import {
|
|
64
|
+
advanceIncrementalParse,
|
|
65
|
+
buildCoreRemarkPlugins,
|
|
66
|
+
buildCoreRehypePlugins,
|
|
67
|
+
buildCoreRemarkRehypeOptions,
|
|
68
|
+
defaultEnginePlugins,
|
|
69
|
+
sanitizeSchema,
|
|
70
|
+
} from '@ai-react-markdown/engine';
|
|
71
|
+
|
|
72
|
+
const options = {
|
|
73
|
+
remarkPlugins: buildCoreRemarkPlugins(defaultEnginePlugins),
|
|
74
|
+
rehypePlugins: buildCoreRehypePlugins(sanitizeSchema, ''),
|
|
75
|
+
remarkRehypeOptions: buildCoreRemarkRehypeOptions(false),
|
|
76
|
+
depsKey: [],
|
|
77
|
+
defListEnabled: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let state = null;
|
|
81
|
+
for (const frame of ['# Hello', '# Hello\n\nworld', '# Hello\n\nworld and more']) {
|
|
82
|
+
const result = advanceIncrementalParse(state, frame, options);
|
|
83
|
+
state = result.nextState;
|
|
84
|
+
// result.hast — the full-document hast for this frame
|
|
85
|
+
// result.usedIncremental / result.boundary — whether the frame spliced, and where
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`AdvanceOptions` is documented in `incrementalParse/advanceIncrementalParse.ts`; the React renderer's `MarkdownContent` is the reference consumer.
|
|
90
|
+
|
|
91
|
+
## Verification
|
|
92
|
+
|
|
93
|
+
The incremental engine ships with a five-layer equivalence stack (fixture pins, fuzz arbiter, direction battery, exhaustive census, arbiter-sensitivity meta-suite) plus a release-gate soak (`scripts/run-soak.sh`); the full record lives in `src/experiments/prefixFreeze/README.md`. Every reachable divergence found so far is pinned as a deterministic test.
|
|
94
|
+
|
|
13
95
|
## Runtime support
|
|
14
96
|
|
|
15
97
|
Pure computation over strings and syntax trees: no DOM access, no
|
|
16
98
|
Node-only APIs, and no unguarded environment reads. Runs in browsers,
|
|
17
99
|
Node, workers, and embedded JS runtimes (e.g. Hermes/JavaScriptCore).
|
|
18
100
|
|
|
101
|
+
## Versioning
|
|
102
|
+
|
|
103
|
+
Lockstep with `@ai-react-markdown/core`, which pins this package **exactly** — the export surface follows what core consumes and may change in any release before 3.0.0 (see the status note above). Release notes: [release highlights](https://github.com/AIEPhoenix/ai-react-markdown/blob/main/docs/release-highlights.md).
|
|
104
|
+
|
|
105
|
+
## Package family
|
|
106
|
+
|
|
107
|
+
| Package | Role | Version policy |
|
|
108
|
+
| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
109
|
+
| [`@ai-react-markdown/core`](https://www.npmjs.com/package/@ai-react-markdown/core) | The React renderer — `<AIMarkdown>`, `<AIMarkdownSmoothStream>`, `<AIMarkdownDocuments>`, hooks, providers | Release train |
|
|
110
|
+
| [`@ai-react-markdown/mantine`](https://www.npmjs.com/package/@ai-react-markdown/mantine) | Mantine UI bindings — themed typography, code-highlight tabs, Mermaid, color-scheme wiring | Release train (lockstep with core) |
|
|
111
|
+
| [`@ai-react-markdown/engine`](https://www.npmjs.com/package/@ai-react-markdown/engine) | Framework-agnostic engine — incremental parsing, LaTeX preprocessing, plugin pipeline, cross-chunk registry | Release train (lockstep, pinned exactly by core; internal supplier) |
|
|
112
|
+
| [`@ai-react-markdown/remark-mark-highlight`](https://www.npmjs.com/package/@ai-react-markdown/remark-mark-highlight) | remark plugin for `==mark==` highlight syntax | Independent semver |
|
|
113
|
+
|
|
19
114
|
## License
|
|
20
115
|
|
|
21
116
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -269,6 +269,11 @@ var defaultUrlTransform = (value) => {
|
|
|
269
269
|
var import_micromark_util_html_tag_name = require("micromark-util-html-tag-name");
|
|
270
270
|
var import_micromark_util_normalize_identifier = require("micromark-util-normalize-identifier");
|
|
271
271
|
var TYPE6_NAMES = new Set(import_micromark_util_html_tag_name.htmlBlockNames);
|
|
272
|
+
var TABLE_PART_NAMES = /* @__PURE__ */ new Set(["td", "th", "tr", "tbody", "thead", "tfoot", "caption", "col", "colgroup"]);
|
|
273
|
+
var TYPE1_NAMES = /* @__PURE__ */ new Set(["script", "pre", "style", "textarea"]);
|
|
274
|
+
var TYPE6_START_RE = /^<\/?([A-Za-z][A-Za-z0-9-]*)(?:[ \t\r]|\/?>|$)/;
|
|
275
|
+
var TYPE1_START_RE = /^<(script|pre|style|textarea)(?:[ \t\r]|>|$)/i;
|
|
276
|
+
var TYPE7_LINE_RE = /^<(\/?)([A-Za-z][A-Za-z0-9-]*)(?:[ \t\r][^>]*|\/)?>[ \t\r]*$/;
|
|
272
277
|
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
273
278
|
"area",
|
|
274
279
|
"base",
|
|
@@ -407,7 +412,11 @@ function freshCheckpoint(defListEnabled, mathFlow, referenceTaint) {
|
|
|
407
412
|
htmlFlowSinceBlank: false,
|
|
408
413
|
htmlSeamPending: false,
|
|
409
414
|
phasePoisonedAt: Infinity,
|
|
410
|
-
pendingTruncatedTags: []
|
|
415
|
+
pendingTruncatedTags: [],
|
|
416
|
+
pendingTruncatedCloses: [],
|
|
417
|
+
tagAcrossLines: false,
|
|
418
|
+
tagAcrossLinesIndent: 0,
|
|
419
|
+
htmlFlowReal: false
|
|
411
420
|
};
|
|
412
421
|
}
|
|
413
422
|
function isPlausibleLinkDefRest(rest) {
|
|
@@ -704,6 +713,8 @@ function processConfirmedLine(cp, ln, text) {
|
|
|
704
713
|
for (const tag of cp.pendingTruncatedTags) applyTag(tag, true);
|
|
705
714
|
cp.pendingTruncatedTags = [];
|
|
706
715
|
}
|
|
716
|
+
cp.pendingTruncatedCloses = [];
|
|
717
|
+
cp.tagAcrossLines = false;
|
|
707
718
|
cp.blankRun += 1;
|
|
708
719
|
cp.lastBlankStart = ln.start;
|
|
709
720
|
cp.candidates.push({
|
|
@@ -717,6 +728,7 @@ function processConfirmedLine(cp, ln, text) {
|
|
|
717
728
|
cp.paragraphHasUnpairedRun = false;
|
|
718
729
|
cp.openBracket = null;
|
|
719
730
|
cp.htmlFlowSinceBlank = false;
|
|
731
|
+
cp.htmlFlowReal = false;
|
|
720
732
|
cp.prevLineBlank = true;
|
|
721
733
|
cp.prevLineWasText = false;
|
|
722
734
|
cp.prevLineWasValidDef = false;
|
|
@@ -732,6 +744,16 @@ function processConfirmedLine(cp, ln, text) {
|
|
|
732
744
|
if (tagStart) {
|
|
733
745
|
cp.htmlFlowSinceBlank = true;
|
|
734
746
|
if (!TYPE6_NAMES.has(tagStart[1].toLowerCase())) cp.hazardVerdict = true;
|
|
747
|
+
if (!cp.htmlFlowReal) {
|
|
748
|
+
const t = mdTrimStart(ln.text);
|
|
749
|
+
const t6 = TYPE6_START_RE.exec(t);
|
|
750
|
+
const t7 = TYPE7_LINE_RE.exec(t);
|
|
751
|
+
if (t6 !== null && TYPE6_NAMES.has(t6[1].toLowerCase()) || TYPE1_START_RE.test(t) || // Type 7 cannot interrupt a paragraph, and excludes the raw-text
|
|
752
|
+
// names (those are type 1 as start tags, paragraph as end tags).
|
|
753
|
+
t7 !== null && !cp.prevLineWasText && !TYPE1_NAMES.has(t7[2].toLowerCase())) {
|
|
754
|
+
cp.htmlFlowReal = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
735
757
|
}
|
|
736
758
|
const inRawText = cp.htmlFlowSinceBlank || rawOpenAtLineStart;
|
|
737
759
|
const rawFlowStart = ln.indent <= 3 && /^<(?:!--|\?|![A-Za-z]|!\[CDATA\[)/.test(mdTrimStart(ln.text));
|
|
@@ -872,7 +894,21 @@ ${cont(scanText)}` };
|
|
|
872
894
|
for (const [from, to] of rawSpans) {
|
|
873
895
|
tagText = tagText.slice(0, from) + " ".repeat(to - from) + tagText.slice(to);
|
|
874
896
|
}
|
|
875
|
-
|
|
897
|
+
let skipTagScan = false;
|
|
898
|
+
if (cp.tagAcrossLines) {
|
|
899
|
+
if (ln.indent < cp.tagAcrossLinesIndent) poisonRawDivergence();
|
|
900
|
+
const gt = ln.text.indexOf(">");
|
|
901
|
+
if (gt === -1) {
|
|
902
|
+
skipTagScan = true;
|
|
903
|
+
} else {
|
|
904
|
+
if (/["']/.test(ln.text.slice(0, gt))) poisonRawDivergence();
|
|
905
|
+
for (const tag of cp.pendingTruncatedCloses) applyTag(tag, true);
|
|
906
|
+
cp.pendingTruncatedCloses = [];
|
|
907
|
+
cp.tagAcrossLines = false;
|
|
908
|
+
tagText = " ".repeat(gt + 1) + tagText.slice(gt + 1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (!skipTagScan) {
|
|
876
912
|
TAG_OR_COMMENT_RE.lastIndex = 0;
|
|
877
913
|
let m;
|
|
878
914
|
let lastCommentOpenerIdx = -1;
|
|
@@ -902,6 +938,7 @@ ${cont(scanText)}` };
|
|
|
902
938
|
if (cp.commentOpen) continue;
|
|
903
939
|
const closing = m[1] === "/";
|
|
904
940
|
const tag = m[2].toLowerCase();
|
|
941
|
+
if (TABLE_PART_NAMES.has(tag)) cp.phasePoisonedAt = Math.min(cp.phasePoisonedAt, ln.start + m.index);
|
|
905
942
|
const selfClosing = m[3] !== void 0 && /\/\s*$/.test(m[3]);
|
|
906
943
|
if (VOID_TAGS.has(tag) || selfClosing) continue;
|
|
907
944
|
applyTag(tag, closing);
|
|
@@ -922,6 +959,7 @@ ${cont(scanText)}` };
|
|
|
922
959
|
if (startMasked || wholeVisible || inRaw(mr.index) || cp.commentOpen) continue;
|
|
923
960
|
const closing = mr[1] === "/";
|
|
924
961
|
const tag = mr[2].toLowerCase();
|
|
962
|
+
if (TABLE_PART_NAMES.has(tag)) cp.phasePoisonedAt = Math.min(cp.phasePoisonedAt, ln.start + mr.index);
|
|
925
963
|
const selfClosing = mr[3] !== void 0 && /\/\s*$/.test(mr[3]);
|
|
926
964
|
if (VOID_TAGS.has(tag) || selfClosing) continue;
|
|
927
965
|
applyTag(tag, closing);
|
|
@@ -937,7 +975,14 @@ ${cont(scanText)}` };
|
|
|
937
975
|
if (m2) {
|
|
938
976
|
const closing = m2[1] === "/";
|
|
939
977
|
const tag = m2[2].toLowerCase();
|
|
940
|
-
if (
|
|
978
|
+
if (TABLE_PART_NAMES.has(tag)) cp.phasePoisonedAt = Math.min(cp.phasePoisonedAt, ln.start + lastLt);
|
|
979
|
+
if (cp.htmlFlowReal) {
|
|
980
|
+
cp.tagAcrossLines = true;
|
|
981
|
+
cp.tagAcrossLinesIndent = ln.indent;
|
|
982
|
+
}
|
|
983
|
+
if (closing) {
|
|
984
|
+
if (!VOID_TAGS.has(tag) && cp.htmlFlowReal) cp.pendingTruncatedCloses.push(tag);
|
|
985
|
+
} else if (!VOID_TAGS.has(tag)) {
|
|
941
986
|
applyTag(tag, closing);
|
|
942
987
|
const rawLastLt = ln.text.lastIndexOf("<");
|
|
943
988
|
const rawTruncated = rawLastLt !== -1 && !ln.text.includes(">", rawLastLt);
|
|
@@ -1480,6 +1525,9 @@ function countTrailingNewlines(value) {
|
|
|
1480
1525
|
function countNewlines(text, end = text.length) {
|
|
1481
1526
|
let count = 0;
|
|
1482
1527
|
for (let i = text.indexOf("\n"); i !== -1 && i < end; i = text.indexOf("\n", i + 1)) count += 1;
|
|
1528
|
+
for (let i = text.indexOf("\r"); i !== -1 && i < end; i = text.indexOf("\r", i + 1)) {
|
|
1529
|
+
if (text.charCodeAt(i + 1) !== 10) count += 1;
|
|
1530
|
+
}
|
|
1483
1531
|
return count;
|
|
1484
1532
|
}
|
|
1485
1533
|
|
|
@@ -1599,7 +1647,7 @@ function normalizeId(s) {
|
|
|
1599
1647
|
return (0, import_micromark_util_normalize_identifier3.normalizeIdentifier)(s);
|
|
1600
1648
|
}
|
|
1601
1649
|
function normalizeForMatch(s) {
|
|
1602
|
-
return (0, import_micromark_util_normalize_identifier3.normalizeIdentifier)(s.replace(/\\(
|
|
1650
|
+
return (0, import_micromark_util_normalize_identifier3.normalizeIdentifier)(s.replace(/\\([!-/:-@[-`{-~])/g, "$1"));
|
|
1603
1651
|
}
|
|
1604
1652
|
|
|
1605
1653
|
// src/components/collectDefLabels.ts
|
|
@@ -4025,10 +4073,16 @@ function lineIndentBefore(content, pos) {
|
|
|
4025
4073
|
function findClosingBacktickRun(content, start, n) {
|
|
4026
4074
|
let i = start;
|
|
4027
4075
|
while (i < content.length) {
|
|
4028
|
-
|
|
4076
|
+
const ch = content[i];
|
|
4077
|
+
if (ch === "`") {
|
|
4029
4078
|
const runLen = getRepeatedMarkerLength(content, i, "`");
|
|
4030
4079
|
if (runLen === n) return i;
|
|
4031
4080
|
i += runLen;
|
|
4081
|
+
} else if (ch === "\n") {
|
|
4082
|
+
let j = i + 1;
|
|
4083
|
+
while (j < content.length && (content[j] === " " || content[j] === " " || content[j] === "\r")) j += 1;
|
|
4084
|
+
if (j >= content.length || content[j] === "\n") return -1;
|
|
4085
|
+
i += 1;
|
|
4032
4086
|
} else {
|
|
4033
4087
|
i += 1;
|
|
4034
4088
|
}
|
|
@@ -4150,16 +4204,20 @@ function escapeCurrencyDollarSigns(text) {
|
|
|
4150
4204
|
currentLineProcessed += segment;
|
|
4151
4205
|
}
|
|
4152
4206
|
let needEscape = true;
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4207
|
+
const restStart = match.index + 1;
|
|
4208
|
+
const restEnd = i < currencyMatches.length - 1 ? currencyMatches[i + 1].index : text.length;
|
|
4209
|
+
let firstLineBeforeNextMatch = "";
|
|
4210
|
+
if (restEnd - restStart > 0) {
|
|
4211
|
+
let eol = restEnd;
|
|
4212
|
+
for (let k = restStart; k < restEnd; k++) {
|
|
4213
|
+
const c = text.charCodeAt(k);
|
|
4214
|
+
if (c === 10 || c === 13) {
|
|
4215
|
+
eol = k;
|
|
4216
|
+
break;
|
|
4217
|
+
}
|
|
4158
4218
|
}
|
|
4159
|
-
|
|
4160
|
-
restBeforeNextMatchOrEnd = text.substring(match.index + 1);
|
|
4219
|
+
firstLineBeforeNextMatch = text.substring(restStart, eol);
|
|
4161
4220
|
}
|
|
4162
|
-
const firstLineBeforeNextMatch = restBeforeNextMatchOrEnd.split(/\r\n|\r|\n/g)[0];
|
|
4163
4221
|
if (Array.from(firstLineBeforeNextMatch.matchAll(NO_ESCAPED_DOLLAR_REGEX)).length % 2 !== 0) {
|
|
4164
4222
|
const wholeLineBeforeNextMatchWithoutCurrentDollar = currentLineProcessed + firstLineBeforeNextMatch;
|
|
4165
4223
|
if (Array.from(wholeLineBeforeNextMatchWithoutCurrentDollar.matchAll(NO_ESCAPED_DOLLAR_REGEX)).length % 2 !== 0) {
|
|
@@ -4325,39 +4383,49 @@ function hasUnclosedTextCommand(text) {
|
|
|
4325
4383
|
return false;
|
|
4326
4384
|
}
|
|
4327
4385
|
var RESIDUAL_OPEN_BRACKET_RE = /(?<!!)\\\[/;
|
|
4328
|
-
|
|
4386
|
+
var LEADING_DOUBLE_DOLLAR_RE = /^\s*\$\$/;
|
|
4387
|
+
function processSliceInstrumented(slice, probe = true) {
|
|
4329
4388
|
const segments = splitByProtectedRegions(slice);
|
|
4330
|
-
|
|
4389
|
+
const parts = [];
|
|
4331
4390
|
let quiescent = true;
|
|
4332
4391
|
let truncatedAtSeamStart = false;
|
|
4333
4392
|
for (let index = 0; index < segments.length; index++) {
|
|
4334
4393
|
const segment = segments[index];
|
|
4335
4394
|
if (segment.isCode) {
|
|
4336
|
-
|
|
4395
|
+
parts.push(segment.text);
|
|
4337
4396
|
continue;
|
|
4338
4397
|
}
|
|
4339
4398
|
let text = segment.text;
|
|
4340
4399
|
text = escapeMhchemCommands(text);
|
|
4341
4400
|
text = escapeCurrencyDollarSigns(text);
|
|
4342
4401
|
text = convertLatexDelimiters(text);
|
|
4343
|
-
if (RESIDUAL_OPEN_BRACKET_RE.test(text)) quiescent = false;
|
|
4402
|
+
if (probe && RESIDUAL_OPEN_BRACKET_RE.test(text)) quiescent = false;
|
|
4344
4403
|
text = escapeLatexPipes(text);
|
|
4345
|
-
if (findUnclosedDelimiterStart(text, "both") !== -1) quiescent = false;
|
|
4404
|
+
if (probe && findUnclosedDelimiterStart(text, "both") !== -1) quiescent = false;
|
|
4346
4405
|
text = escapeLatexPipesInUnclosed(text);
|
|
4347
|
-
if (hasUnclosedTextCommand(text)) quiescent = false;
|
|
4406
|
+
if (probe && hasUnclosedTextCommand(text)) quiescent = false;
|
|
4348
4407
|
text = escapeTextUnderscores(text);
|
|
4349
4408
|
text = convertSingleToDoubleDollar(text);
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4409
|
+
if (probe || index === 0 && LEADING_DOUBLE_DOLLAR_RE.test(text)) {
|
|
4410
|
+
const unclosedDouble = findUnclosedDelimiterStart(text, "double-only");
|
|
4411
|
+
if (unclosedDouble !== -1) {
|
|
4412
|
+
quiescent = false;
|
|
4413
|
+
if (index === 0 && text.slice(0, unclosedDouble).trim() === "") {
|
|
4414
|
+
truncatedAtSeamStart = true;
|
|
4415
|
+
}
|
|
4355
4416
|
}
|
|
4356
4417
|
}
|
|
4357
4418
|
text = truncateUnclosedLatexBlock(text);
|
|
4358
|
-
|
|
4419
|
+
parts.push(text);
|
|
4359
4420
|
}
|
|
4360
|
-
return { out, quiescent, truncatedAtSeamStart };
|
|
4421
|
+
return { out: parts.join(""), quiescent, truncatedAtSeamStart };
|
|
4422
|
+
}
|
|
4423
|
+
function isBlankRawLine(text, from, to) {
|
|
4424
|
+
for (let i = from; i < to; i++) {
|
|
4425
|
+
const c = text.charCodeAt(i);
|
|
4426
|
+
if (c !== 32 && c !== 9 && c !== 13) return false;
|
|
4427
|
+
}
|
|
4428
|
+
return true;
|
|
4361
4429
|
}
|
|
4362
4430
|
function findRawSafeCut(active) {
|
|
4363
4431
|
const segments = splitByProtectedRegions(active);
|
|
@@ -4372,10 +4440,12 @@ function findRawSafeCut(active) {
|
|
|
4372
4440
|
continue;
|
|
4373
4441
|
}
|
|
4374
4442
|
const text = segment.text;
|
|
4443
|
+
let atLineStart = offset === 0 || active.charCodeAt(offset - 1) === 10;
|
|
4375
4444
|
let lineStart = 0;
|
|
4376
4445
|
while (lineStart <= text.length) {
|
|
4377
4446
|
const nl = text.indexOf("\n", lineStart);
|
|
4378
4447
|
const lineEnd = nl === -1 ? text.length : nl;
|
|
4448
|
+
if (atLineStart && nl !== -1 && isBlankRawLine(text, lineStart, lineEnd)) backtickHazard = false;
|
|
4379
4449
|
for (let i = lineStart; i < lineEnd; i++) {
|
|
4380
4450
|
const ch = text[i];
|
|
4381
4451
|
if (ch === "`") backtickHazard = true;
|
|
@@ -4388,6 +4458,7 @@ function findRawSafeCut(active) {
|
|
|
4388
4458
|
if (nl === -1) break;
|
|
4389
4459
|
if (!backtickHazard && !latentLt) lastCut = offset + nl + 1;
|
|
4390
4460
|
lineStart = nl + 1;
|
|
4461
|
+
atLineStart = true;
|
|
4391
4462
|
}
|
|
4392
4463
|
offset += text.length;
|
|
4393
4464
|
}
|
|
@@ -4396,11 +4467,14 @@ function findRawSafeCut(active) {
|
|
|
4396
4467
|
var DEFAULT_FREEZE_ATTEMPT_THRESHOLD = 512;
|
|
4397
4468
|
function createIncrementalLatexPreprocessor(options) {
|
|
4398
4469
|
const freezeThreshold = options?.freezeThreshold ?? DEFAULT_FREEZE_ATTEMPT_THRESHOLD;
|
|
4470
|
+
const onAttempt = options?.onAttempt;
|
|
4471
|
+
const backoff = options?.backoff ?? true;
|
|
4399
4472
|
let prevSource = "";
|
|
4400
4473
|
let prevOutput = "";
|
|
4401
4474
|
let frozenSrcEnd = 0;
|
|
4402
4475
|
let frozenOut = "";
|
|
4403
4476
|
let triggered = false;
|
|
4477
|
+
let nextAttemptLen = 0;
|
|
4404
4478
|
return function incrementalPreprocessLaTeX(source) {
|
|
4405
4479
|
if (source === prevSource) return prevOutput;
|
|
4406
4480
|
const isAppend = source.length > prevSource.length && source.startsWith(prevSource);
|
|
@@ -4408,6 +4482,7 @@ function createIncrementalLatexPreprocessor(options) {
|
|
|
4408
4482
|
frozenSrcEnd = 0;
|
|
4409
4483
|
frozenOut = "";
|
|
4410
4484
|
triggered = false;
|
|
4485
|
+
nextAttemptLen = 0;
|
|
4411
4486
|
}
|
|
4412
4487
|
if (!triggered) {
|
|
4413
4488
|
const checkFrom = isAppend ? Math.max(0, prevSource.length - 1) : 0;
|
|
@@ -4419,18 +4494,26 @@ function createIncrementalLatexPreprocessor(options) {
|
|
|
4419
4494
|
triggered = true;
|
|
4420
4495
|
}
|
|
4421
4496
|
let active = source.slice(frozenSrcEnd);
|
|
4422
|
-
if (active.length > freezeThreshold) {
|
|
4497
|
+
if (active.length > freezeThreshold && active.length >= nextAttemptLen) {
|
|
4498
|
+
const activeLength = active.length;
|
|
4499
|
+
let advanced = false;
|
|
4500
|
+
let frozenBytes = 0;
|
|
4501
|
+
const freeze = (cut2, slice) => {
|
|
4502
|
+
frozenOut += slice.out;
|
|
4503
|
+
frozenSrcEnd += cut2;
|
|
4504
|
+
active = source.slice(frozenSrcEnd);
|
|
4505
|
+
advanced = true;
|
|
4506
|
+
frozenBytes = cut2;
|
|
4507
|
+
};
|
|
4423
4508
|
const cut = findRawSafeCut(active);
|
|
4424
4509
|
if (cut > 0) {
|
|
4425
4510
|
const candidate = processSliceInstrumented(active.slice(0, cut));
|
|
4426
|
-
if (candidate.quiescent)
|
|
4427
|
-
frozenOut += candidate.out;
|
|
4428
|
-
frozenSrcEnd += cut;
|
|
4429
|
-
active = source.slice(frozenSrcEnd);
|
|
4430
|
-
}
|
|
4511
|
+
if (candidate.quiescent) freeze(cut, candidate);
|
|
4431
4512
|
}
|
|
4513
|
+
nextAttemptLen = advanced || !backoff ? 0 : active.length * 2;
|
|
4514
|
+
onAttempt?.({ activeLength, frozenBytes });
|
|
4432
4515
|
}
|
|
4433
|
-
const tail = processSliceInstrumented(active);
|
|
4516
|
+
const tail = processSliceInstrumented(active, false);
|
|
4434
4517
|
const head = tail.truncatedAtSeamStart ? frozenOut.replace(/\s+$/, "") : frozenOut;
|
|
4435
4518
|
const out = head + tail.out;
|
|
4436
4519
|
prevSource = source;
|