@ai-matrx/content-ir 0.13.0 → 0.14.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.1 — 2026-09-25
4
+
5
+ **Single-dollar math: an adversarial review of the widened rule** (RC-B2's
6
+ single-letter shapes, shipped in 0.14.0). Stored-text impact, every row diffed
7
+ 0.13.0 → 0.14.x: 11 chat text parts gain 37 math spans, 0 lost; notes, agent
8
+ messages, templates and skills unchanged; every gained span is a real shape
9
+ (`f(x)`, `(x, y)`, `(-x, y)`, `f(x-y)`). Two synthetic false positives found
10
+ by probing (none in stored data) are closed: a contraction (`$I'm$` — a prime
11
+ is never followed by a letter) and an abbreviation (`$e.g. (x)$` — a lone
12
+ letter is never followed by a period) stay text. Guard: 6 new cases in
13
+ `__tests__/single-dollar-math.test.ts` (3 red before).
14
+
15
+ **Consumer action:** none.
16
+
17
+ ## 0.14.0 — 2026-09-25
18
+
19
+ **`/source` — RC-B3 polish: the re-verification residuals, and the verifier's
20
+ attacks made permanent** (common-docs
21
+ `projects/rich-content-unification/evidence/verify-RC-B3.md` § Re-verification).
22
+ Each residual had a failing test first (R1–R3 red on 0.13.0; the new
23
+ cross-language vectors fail 10 cases on the old Python twin):
24
+
25
+ - **R1** — an insertion carrying an exact copy of the next island at its
26
+ expected offset no longer causes a false `integrity` refusal: an island's
27
+ candidate positions are scored by how much of the original text still
28
+ surrounds them.
29
+ - **R2** — `tokenizeSource(text, { streaming: true })`: the last,
30
+ still-unterminated line closes no fence (a bare ```` ``` ```` there may grow
31
+ into ```` ```bash ````). Stored text omits the option.
32
+ - **R3** — `listIslands` loops instead of spreading, so a block with 150k
33
+ inline islands saves without a `RangeError`.
34
+ - **R4** — the fence rule defines its whitespace: `FENCE_WHITESPACE`
35
+ (CommonMark ASCII whitespace) and `trimFenceLine`, which every caller uses
36
+ in place of a language's `trim()`. The vectors carry U+FEFF, U+0085,
37
+ U+00A0, U+001C–U+001F and U+2028 lines; aidream matrx-ai's Python twin
38
+ gained `FENCE_WHITESPACE` / `trim_fence_line` and passes them.
39
+ - **R5** — tilde and odd-whitespace fences: matrx-frontend's
40
+ `source-covers-renderer` property test proves the tokenizer locks at least
41
+ every fenced block the renderer draws (both now share `trimFenceLine`).
42
+ - **Permanent attack suite** — `__tests__/source-attack.test.ts`: seeded fuzz
43
+ of rejoin identity, offsets (independently counted), surrogate safety,
44
+ no-edit/identity saves, splice verdicts, island refusals, misaligned spans,
45
+ streaming prefixes (settled islands never move) and pathological timing
46
+ (budget + linear scaling), bounded in CI; `SOURCE_ATTACK=long` runs the
47
+ verifier-scale version.
48
+
49
+ **Consumer action:** fence-line classification callers trim with
50
+ `trimFenceLine(line)` (matrx-frontend's splitters do as of this release).
51
+
52
+ **`/source` — islands for the extended markdown syntax** (RC-B8, common-docs
53
+ `projects/rich-content-unification/REGISTER.md`). The renderer now draws
54
+ callouts, directives, front matter, footnotes and wikilinks; an editor must
55
+ never re-serialize their grammar, so each is now an island:
56
+
57
+ - `directive` (block) — a generic-directive container `:::name[label]{attrs}`
58
+ … `:::` with nesting counted (tabs, columns, details, figures, callouts,
59
+ TOC), a leaf `::name` line (`meta.leaf`), or a MkDocs admonition
60
+ `!!! type "Title"` / `??? type` with its indented body (`meta.syntax: "mkdocs"`).
61
+ An unclosed container runs to the end with `complete: false` (streaming).
62
+ - `callout` (block) — a blockquote opening `> [!TYPE]` (GitHub alert /
63
+ Obsidian callout; `meta.name`, `meta.fold`).
64
+ - `footnote_def` (block) — `[^id]: …` with its continuation lines (`meta.id`).
65
+ - `front_matter` now also recognizes TOML (`+++`); `meta.format` is `yaml` | `toml`.
66
+ - `wikilink` (inline) — `[[Page]]`, `[[Page|alias]]`, `![[Page]]`
67
+ (`meta.target`, `meta.embed`); never across a line, never inside code.
68
+
69
+ CSV/TSV fences were already `fence` islands (`meta.lang`). Guard:
70
+ `__tests__/source-markdown-syntax-islands.test.ts` (7 of 10 red on 0.13.0).
71
+
72
+ **Consumer action:** an exhaustive `switch` over `IslandType` /
73
+ `BlockIslandType` gains the new members; nothing else changes (joinSource is
74
+ still byte-identical, the corpus check stays green).
75
+
76
+ **Single-dollar math: single-letter math shapes are math** (RC-B2 verification,
77
+ common-docs `projects/rich-content-unification/evidence/verify-RC-B2.md`).
78
+ `isSingleDollarMath` now also accepts a shape built only from single-letter
79
+ identifiers (primes, standard function names), digits, parentheses, commas and
80
+ `- * /` that applies, primes or juxtaposes them — `$f(g(x))$`, `$u v$`,
81
+ `$f'(x)$`, `$(x, y)$`, `$ln(x)$`. Words keep it text (`$Tipping (fee)$`), and
82
+ the pandoc spacing / digit rules still keep currency (`$5 and $10`, `$5-$10`)
83
+ as text. Guard: `__tests__/single-dollar-math.test.ts` (5 of 12 red before).
84
+
85
+ **Consumer action:** none — adopt the release; matrx-frontend's math
86
+ normalizer and `/source` tokenizer pick the rule up through the import.
87
+
3
88
  ## 0.13.0 — 2026-09-25
4
89
 
5
90
  **`/source` — fixes from the RC-B3 independent verification**
package/dist/index.cjs CHANGED
@@ -5184,11 +5184,22 @@ var NESTING_FENCE_LANGUAGES = /* @__PURE__ */ new Set([
5184
5184
  function fenceNestsInnerFences(language) {
5185
5185
  return !!language && NESTING_FENCE_LANGUAGES.has(language.toLowerCase());
5186
5186
  }
5187
+ var FENCE_WHITESPACE = " \n\r\f\v";
5188
+ function isFenceWhitespace(ch) {
5189
+ return ch !== void 0 && ch !== "" && FENCE_WHITESPACE.includes(ch);
5190
+ }
5191
+ function trimFenceLine(line) {
5192
+ let start = 0;
5193
+ let end = line.length;
5194
+ while (start < end && isFenceWhitespace(line[start])) start++;
5195
+ while (end > start && isFenceWhitespace(line[end - 1])) end--;
5196
+ return start === 0 && end === line.length ? line : line.slice(start, end);
5197
+ }
5187
5198
  function classifyInnerFenceLine(trimmed, openTicks, nests, nestedDepth) {
5188
5199
  let ticks = 0;
5189
5200
  while (ticks < trimmed.length && trimmed[ticks] === "`") ticks++;
5190
5201
  if (ticks < 3) return "content";
5191
- const info = trimmed.slice(ticks).trim();
5202
+ const info = trimFenceLine(trimmed.slice(ticks));
5192
5203
  if (info === "") {
5193
5204
  if (ticks < openTicks) return "content";
5194
5205
  if (nests && nestedDepth > 0) return "close-nested";
@@ -5201,13 +5212,43 @@ function classifyInnerFenceLine(trimmed, openTicks, nests, nestedDepth) {
5201
5212
  // source/math.ts
5202
5213
  var TEX_SIGNAL = /\\[A-Za-z]+|[\\^_{}=<>+]/;
5203
5214
  var LONE_VARIABLE = /^[A-Za-z](?:'|[0-9])?$/;
5215
+ var MATH_SHAPE_CHARS = /^[A-Za-z0-9'\s(),.*\/-]+$/;
5216
+ var FUNCTION_NAMES = /* @__PURE__ */ new Set([
5217
+ "sin",
5218
+ "cos",
5219
+ "tan",
5220
+ "sec",
5221
+ "csc",
5222
+ "cot",
5223
+ "log",
5224
+ "ln",
5225
+ "exp",
5226
+ "lim",
5227
+ "max",
5228
+ "min",
5229
+ "det",
5230
+ "gcd",
5231
+ "mod",
5232
+ "arg",
5233
+ "sinh",
5234
+ "cosh",
5235
+ "tanh"
5236
+ ]);
5237
+ function isMathShape(content) {
5238
+ if (!MATH_SHAPE_CHARS.test(content)) return false;
5239
+ const words = content.match(/[A-Za-z]+/g);
5240
+ if (!words) return false;
5241
+ if (!words.every((w) => w.length === 1 || FUNCTION_NAMES.has(w))) return false;
5242
+ if (/[A-Za-z]'+[A-Za-z]/.test(content) || /(?:^|[\s(,])[A-Za-z]\.(?:[A-Za-z]\.|\s|$)/.test(content)) return false;
5243
+ return /[A-Za-z]'*\(|[A-Za-z]'|\b[A-Za-z]\s+[A-Za-z]\b|\([^)]*[A-Za-z][^)]*\)/.test(content);
5244
+ }
5204
5245
  function isSingleDollarMath(content, after) {
5205
5246
  if (!content || content.length > 400) return false;
5206
5247
  if (/^\s/.test(content) || /\s$/.test(content)) return false;
5207
5248
  if (after !== void 0 && /[0-9]/.test(after)) return false;
5208
5249
  if (LONE_VARIABLE.test(content)) return true;
5209
5250
  if (/^[0-9]/.test(content)) return /\\[A-Za-z]+|[\\^_{}]/.test(content);
5210
- return TEX_SIGNAL.test(content);
5251
+ return TEX_SIGNAL.test(content) || isMathShape(content);
5211
5252
  }
5212
5253
  function singleDollarMathEnd(text, open, limit = text.length) {
5213
5254
  const bound = Math.min(limit, open + 403);
@@ -5443,6 +5484,10 @@ var XmlContainerTracker = class {
5443
5484
 
5444
5485
  // source/tokenize.ts
5445
5486
  var NOT_WHITESPACE = /\S/;
5487
+ var DIRECTIVE_OPEN = /^(:{2,})([A-Za-z][\w-]*)/;
5488
+ var MKDOCS_OPEN = /^(!!!|\?\?\?\+?)[ \t]+([A-Za-z][\w-]*)/;
5489
+ var CALLOUT_OPEN = /^>[ \t]*\[!([A-Za-z][\w-]*)\]([+-])?/;
5490
+ var FOOTNOTE_DEF = /^\[\^([^\]\s]+)\]:/;
5446
5491
  function countStructuralBraces(source) {
5447
5492
  let opens = 0;
5448
5493
  let closes = 0;
@@ -5570,8 +5615,9 @@ function isSplitterTreeLine(line) {
5570
5615
  }
5571
5616
  var MEDIA_REF_PREFIXES = ["[Image URL:", "[Video URL:", "[Audio URL:"];
5572
5617
  var Tokenizer = class {
5573
- constructor(text) {
5618
+ constructor(text, streaming = false) {
5574
5619
  this.text = text;
5620
+ this.streaming = streaming;
5575
5621
  this.n = text.length;
5576
5622
  this.mathPairs = pairDisplayMath(text);
5577
5623
  let start = 0;
@@ -5590,6 +5636,7 @@ var Tokenizer = class {
5590
5636
  }
5591
5637
  }
5592
5638
  text;
5639
+ streaming;
5593
5640
  n;
5594
5641
  lineStarts = [];
5595
5642
  contentEnds = [];
@@ -5627,6 +5674,10 @@ var Tokenizer = class {
5627
5674
  }
5628
5675
  return lo < list.length ? list[lo] : -1;
5629
5676
  }
5677
+ /** Streaming only: the final line, still waiting for its terminator. */
5678
+ isOpenTail(k) {
5679
+ return this.streaming && k === this.lineStarts.length - 1 && this.lineEnds[k] === this.contentEnds[k];
5680
+ }
5630
5681
  get lineCount() {
5631
5682
  return this.lineStarts.length;
5632
5683
  }
@@ -5708,6 +5759,22 @@ var Tokenizer = class {
5708
5759
  const fm = this.frontMatter(li, t);
5709
5760
  if (fm) return fm;
5710
5761
  }
5762
+ if (indent <= 3 && t.startsWith("::")) {
5763
+ const directive = this.directive(p, li, t);
5764
+ if (directive) return directive;
5765
+ }
5766
+ if (indent === 0 && (t.startsWith("!!!") || t.startsWith("???"))) {
5767
+ const admonition = this.mkdocsAdmonition(p, li, t);
5768
+ if (admonition) return admonition;
5769
+ }
5770
+ if (indent <= 3 && t.startsWith(">")) {
5771
+ const callout = this.callout(p, li, t);
5772
+ if (callout) return callout;
5773
+ }
5774
+ if (atParaStart && indent <= 3 && t.startsWith("[^")) {
5775
+ const footnote = this.footnoteDefinition(p, li, t);
5776
+ if (footnote) return footnote;
5777
+ }
5711
5778
  if (t.startsWith("```")) return this.backtickFence(p, li, t);
5712
5779
  if (indent <= 3 && t.startsWith("~~~")) {
5713
5780
  const tilde = this.tildeFence(p, li, t);
@@ -5745,19 +5812,110 @@ var Tokenizer = class {
5745
5812
  return { type, start, end, complete, meta };
5746
5813
  }
5747
5814
  frontMatter(li, t) {
5748
- if (this.lineText(li) !== "---" || t !== "---") return null;
5815
+ const fence = this.lineText(li);
5816
+ if (fence !== "---" && fence !== "+++" || t !== fence) return null;
5817
+ const toml = fence === "+++";
5749
5818
  let sawContent = false;
5750
5819
  for (let k = li + 1; k < this.lineCount; k++) {
5751
5820
  const line = this.lineText(k);
5752
- if (line === "---" || line === "...") {
5753
- return sawContent ? this.island("front_matter", 0, this.contentEnds[k], true) : null;
5821
+ if (line === fence || !toml && line === "...") {
5822
+ return sawContent ? this.island("front_matter", 0, this.contentEnds[k], true, { format: toml ? "toml" : "yaml" }) : null;
5754
5823
  }
5755
5824
  if (!NOT_WHITESPACE.test(line)) continue;
5756
- if (!/^(?:[A-Za-z0-9_"'-][^:]*:(?:\s|$)|\s+\S|-\s)/.test(line)) return null;
5825
+ const shaped = toml ? /^\s*(?:[A-Za-z0-9_."-]+\s*=|\[|#)/.test(line) : /^(?:[A-Za-z0-9_"'-][^:]*:(?:\s|$)|\s+\S|-\s|#)/.test(line);
5826
+ if (!shaped) return null;
5757
5827
  sawContent = true;
5758
5828
  }
5759
5829
  return null;
5760
5830
  }
5831
+ /**
5832
+ * `:::name[label]{attrs}` … closing `:::` (a colon run at least as long as
5833
+ * the opener's, nesting counted), or a leaf `::name[…]{…}` line. The whole
5834
+ * container is ONE island: its grammar (fence lengths, labels, attributes,
5835
+ * nested tabs) is exactly what a visual editor would re-serialize wrongly.
5836
+ */
5837
+ directive(p, li, t) {
5838
+ const open = DIRECTIVE_OPEN.exec(t);
5839
+ if (!open) return null;
5840
+ const colons = open[1].length;
5841
+ const name = open[2];
5842
+ if (colons === 2) {
5843
+ return this.island("directive", p, this.contentEnds[li], true, { name, leaf: true });
5844
+ }
5845
+ const stack = [colons];
5846
+ for (let k = li + 1; k < this.lineCount; k++) {
5847
+ const line = this.lineText(k).trim();
5848
+ const inner = DIRECTIVE_OPEN.exec(line);
5849
+ if (inner && inner[1].length >= 3) {
5850
+ stack.push(inner[1].length);
5851
+ continue;
5852
+ }
5853
+ const close = /^(:{3,})\s*$/.exec(line);
5854
+ if (close && close[1].length >= stack[stack.length - 1]) {
5855
+ stack.pop();
5856
+ if (stack.length === 0) return this.island("directive", p, this.contentEnds[k], true, { name });
5857
+ }
5858
+ }
5859
+ return this.island("directive", p, this.n, false, { name });
5860
+ }
5861
+ /** MkDocs `!!! type "Title"` / `??? type` / `???+ type` and its indented (or blank-separated indented) body. */
5862
+ mkdocsAdmonition(p, li, t) {
5863
+ const open = MKDOCS_OPEN.exec(t);
5864
+ if (!open) return null;
5865
+ let last = li;
5866
+ for (let k = li + 1; k < this.lineCount; k++) {
5867
+ const line = this.lineText(k);
5868
+ if (/^(?: {4}|\t)/.test(line) && NOT_WHITESPACE.test(line)) {
5869
+ last = k;
5870
+ continue;
5871
+ }
5872
+ if (!NOT_WHITESPACE.test(line)) continue;
5873
+ break;
5874
+ }
5875
+ return this.island("directive", p, this.contentEnds[last], true, {
5876
+ name: open[2].toLowerCase(),
5877
+ syntax: "mkdocs"
5878
+ });
5879
+ }
5880
+ /** A blockquote whose first line is `> [!TYPE]…` — every following `>` line belongs to it. */
5881
+ callout(p, li, t) {
5882
+ const open = CALLOUT_OPEN.exec(t);
5883
+ if (!open) return null;
5884
+ let last = li;
5885
+ for (let k = li + 1; k < this.lineCount; k++) {
5886
+ if (!/^ {0,3}>/.test(this.lineText(k))) break;
5887
+ last = k;
5888
+ }
5889
+ return this.island("callout", p, this.contentEnds[last], true, {
5890
+ name: open[1].toLowerCase(),
5891
+ ...open[2] ? { fold: open[2] === "-" ? "closed" : "open" } : {}
5892
+ });
5893
+ }
5894
+ /** `[^id]: text` plus its continuation lines (indented, or lazy text before a blank line). */
5895
+ footnoteDefinition(p, li, t) {
5896
+ const open = FOOTNOTE_DEF.exec(t);
5897
+ if (!open) return null;
5898
+ let last = li;
5899
+ for (let k = li + 1; k < this.lineCount; k++) {
5900
+ const line = this.lineText(k);
5901
+ if (!NOT_WHITESPACE.test(line)) {
5902
+ let j = k + 1;
5903
+ while (j < this.lineCount && !NOT_WHITESPACE.test(this.lineText(j))) j++;
5904
+ if (j < this.lineCount && /^(?: {4}|\t)/.test(this.lineText(j))) {
5905
+ k = j - 1;
5906
+ continue;
5907
+ }
5908
+ break;
5909
+ }
5910
+ if (/^(?: {4}|\t)/.test(line)) {
5911
+ last = k;
5912
+ continue;
5913
+ }
5914
+ if (FOOTNOTE_DEF.test(line.trimStart()) || this.detectBlock(this.lineStarts[k], k, false)) break;
5915
+ last = k;
5916
+ }
5917
+ return this.island("footnote_def", p, this.contentEnds[last], true, { id: open[1] });
5918
+ }
5761
5919
  /**
5762
5920
  * Backtick fence — the renderer splitter's close rules: JSON-string aware for
5763
5921
  * ```json, and THE nested-fence rule (source/fence-nesting.ts) for markdown
@@ -5786,8 +5944,9 @@ var Tokenizer = class {
5786
5944
  let nestedDepth = 0;
5787
5945
  let sawNested = false;
5788
5946
  for (let k = li + 1; k < this.lineCount; k++) {
5947
+ if (this.isOpenTail(k)) continue;
5789
5948
  const line = this.lineText(k);
5790
- const trimmedLine = line.trim();
5949
+ const trimmedLine = trimFenceLine(line);
5791
5950
  if (trimmedLine.startsWith("```")) {
5792
5951
  const at2 = line.indexOf("```");
5793
5952
  if (isJson) {
@@ -5817,7 +5976,7 @@ var Tokenizer = class {
5817
5976
  }
5818
5977
  }
5819
5978
  const closeTicks = backtickRunLength(line, at);
5820
- const after = line.slice(at + closeTicks).trim();
5979
+ const after = trimFenceLine(line.slice(at + closeTicks));
5821
5980
  if (closeTicks >= openTicks && after === "" && nestedDepth === 0) return k;
5822
5981
  if (isJson) state = jsonStringState(line, 0, line.length, state);
5823
5982
  continue;
@@ -5844,6 +6003,7 @@ var Tokenizer = class {
5844
6003
  if (normalized && SPECIAL_CODE_LANGUAGES.has(normalized)) meta.special = normalized;
5845
6004
  const close = new RegExp(`^ {0,3}~{${ticks},}[ \\t]*$`);
5846
6005
  for (let k = li + 1; k < this.lineCount; k++) {
6006
+ if (this.isOpenTail(k)) continue;
5847
6007
  if (close.test(this.lineText(k))) {
5848
6008
  return this.island("fence", p, this.contentEnds[k], true, meta);
5849
6009
  }
@@ -6233,6 +6393,21 @@ var Tokenizer = class {
6233
6393
  i++;
6234
6394
  continue;
6235
6395
  }
6396
+ if (ch === "[" && text[i + 1] === "[" || ch === "!" && text[i + 1] === "[" && text[i + 2] === "[") {
6397
+ const open = ch === "!" ? i + 3 : i + 2;
6398
+ const close = within("]]", open);
6399
+ const newline = this.next("\n", open);
6400
+ const inner = close === -1 ? "" : text.slice(open, close);
6401
+ if (close !== -1 && (newline === -1 || newline > close) && inner.trim() && !/[[\]]/.test(inner)) {
6402
+ const cut = inner.search(/\\?\|/);
6403
+ const target = (cut < 0 ? inner : inner.slice(0, cut)).trim();
6404
+ inlines.push(
6405
+ this.island("wikilink", i, close + 2, true, ch === "!" ? { target, embed: true } : { target })
6406
+ );
6407
+ i = close + 2;
6408
+ continue;
6409
+ }
6410
+ }
6236
6411
  if (ch === "[") {
6237
6412
  const media = MEDIA_REF_PREFIXES.find((prefix) => text.startsWith(prefix, i));
6238
6413
  if (media) {
@@ -6314,8 +6489,8 @@ var Tokenizer = class {
6314
6489
  return { inlines, breakIsland: null, orphan: false };
6315
6490
  }
6316
6491
  };
6317
- function tokenizeSource(text) {
6318
- const raw = new Tokenizer(text).run();
6492
+ function tokenizeSource(text, options = {}) {
6493
+ const raw = new Tokenizer(text, options.streaming ?? false).run();
6319
6494
  const cp = buildCodePointIndex(text);
6320
6495
  const inline = (island) => ({
6321
6496
  islandType: island.type,
@@ -6364,7 +6539,7 @@ function listIslands(blocks) {
6364
6539
  raw: block.raw
6365
6540
  });
6366
6541
  } else {
6367
- out.push(...block.inlines);
6542
+ for (const inline of block.inlines) out.push(inline);
6368
6543
  }
6369
6544
  }
6370
6545
  return out;
@@ -6400,15 +6575,37 @@ function islandEdit(island, text) {
6400
6575
  function blockRangeEdit(first, last, text) {
6401
6576
  return { start: first.start, end: last.end, text };
6402
6577
  }
6403
- function locateIsland(text, raw, from, expected, expectedFromEnd) {
6404
- if (expected >= from && text.startsWith(raw, expected)) return expected;
6405
- if (expectedFromEnd >= from && text.startsWith(raw, expectedFromEnd)) return expectedFromEnd;
6578
+ var CONTEXT_WINDOW = 64;
6579
+ function locateIsland(text, raw, from, expected, expectedFromEnd, oldText, oldStart, oldEnd) {
6580
+ const candidates = /* @__PURE__ */ new Set();
6581
+ if (expected >= from && text.startsWith(raw, expected)) candidates.add(expected);
6582
+ if (expectedFromEnd >= from && text.startsWith(raw, expectedFromEnd)) candidates.add(expectedFromEnd);
6406
6583
  const after = text.indexOf(raw, Math.max(from, expected));
6584
+ if (after !== -1) candidates.add(after);
6407
6585
  const before = expected > from ? text.lastIndexOf(raw, expected) : -1;
6408
- const beforeOk = before >= from ? before : -1;
6409
- if (after === -1) return beforeOk;
6410
- if (beforeOk === -1) return after;
6411
- return expected - beforeOk <= after - expected ? beforeOk : after;
6586
+ if (before >= from) candidates.add(before);
6587
+ if (candidates.size === 0) return text.indexOf(raw, from);
6588
+ if (candidates.size === 1) return candidates.values().next().value;
6589
+ let best = -1;
6590
+ let bestScore = -1;
6591
+ for (const at of candidates) {
6592
+ let score = 0;
6593
+ for (let k = 0; k < CONTEXT_WINDOW; k++) {
6594
+ const n = text[at + raw.length + k];
6595
+ if (n === void 0 || n !== oldText[oldEnd + k]) break;
6596
+ score++;
6597
+ }
6598
+ for (let k = 1; k <= CONTEXT_WINDOW; k++) {
6599
+ const n = text[at - k];
6600
+ if (n === void 0 || n !== oldText[oldStart - k]) break;
6601
+ score++;
6602
+ }
6603
+ if (score > bestScore || score === bestScore && at === expected) {
6604
+ best = at;
6605
+ bestScore = score;
6606
+ }
6607
+ }
6608
+ return best;
6412
6609
  }
6413
6610
  function commonPrefix(a, b) {
6414
6611
  const max = Math.min(a.length, b.length);
@@ -6485,7 +6682,16 @@ function spliceSave(original, edits, options = {}) {
6485
6682
  let oldCursor2 = edit.start;
6486
6683
  let newCursor2 = 0;
6487
6684
  for (const island of inside) {
6488
- const at = locateIsland(edit.text, island.raw, newCursor2, newCursor2 + (island.start - oldCursor2), edit.text.length - (edit.end - island.start));
6685
+ const at = locateIsland(
6686
+ edit.text,
6687
+ island.raw,
6688
+ newCursor2,
6689
+ newCursor2 + (island.start - oldCursor2),
6690
+ edit.text.length - (edit.end - island.start),
6691
+ original.slice(edit.start, edit.end),
6692
+ island.start - edit.start,
6693
+ island.end - edit.start
6694
+ );
6489
6695
  if (at === -1) {
6490
6696
  const name = island.meta.tag ?? island.meta.name ?? island.meta.lang ?? "";
6491
6697
  throw new SourceSpliceError(
@@ -6628,6 +6834,7 @@ exports.CAPABILITY_BY_CLASS = CAPABILITY_BY_CLASS;
6628
6834
  exports.CLASSES = CLASSES;
6629
6835
  exports.DIRECTIVE_VERSION = DIRECTIVE_VERSION;
6630
6836
  exports.DirectiveDecodeError = DirectiveDecodeError;
6837
+ exports.FENCE_WHITESPACE = FENCE_WHITESPACE;
6631
6838
  exports.IN_CONTENT_CLASSES = IN_CONTENT_CLASSES;
6632
6839
  exports.IR_ENVELOPE_CACHE_VERSION = IR_ENVELOPE_CACHE_VERSION;
6633
6840
  exports.IR_ENVELOPE_KEY = IR_ENVELOPE_KEY;
@@ -6759,6 +6966,7 @@ exports.titleCaseToken = titleCaseToken;
6759
6966
  exports.toCodePointOffset = toCodePointOffset;
6760
6967
  exports.toUtf16Offset = toUtf16Offset;
6761
6968
  exports.tokenizeSource = tokenizeSource;
6969
+ exports.trimFenceLine = trimFenceLine;
6762
6970
  exports.tryDecodeDirective = tryDecodeDirective;
6763
6971
  exports.tryDecodeDirectiveContent = tryDecodeDirectiveContent;
6764
6972
  exports.validateBlockSchemaSavePlan = validateBlockSchemaSavePlan;