@stream-mdx/core 0.0.3 → 0.1.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/dist/index.mjs CHANGED
@@ -202,6 +202,36 @@ function filterAllowedAttributes(attrs) {
202
202
  }
203
203
 
204
204
  // src/inline-parser.ts
205
+ function ensureGlobal(pattern) {
206
+ if (pattern.global) return pattern;
207
+ const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
208
+ return new RegExp(pattern.source, flags);
209
+ }
210
+ function findLastMatch(pattern, value) {
211
+ const re = ensureGlobal(pattern);
212
+ let match = null;
213
+ let next;
214
+ while ((next = re.exec(value)) !== null) {
215
+ match = next;
216
+ }
217
+ return match;
218
+ }
219
+ function findMatchAfter(pattern, value, startIndex) {
220
+ const re = ensureGlobal(pattern);
221
+ re.lastIndex = Math.max(0, startIndex);
222
+ return re.exec(value);
223
+ }
224
+ function isSamePattern(a, b) {
225
+ return a.source === b.source && a.flags === b.flags;
226
+ }
227
+ function countMatches(pattern, value) {
228
+ const re = ensureGlobal(pattern);
229
+ let count = 0;
230
+ while (re.exec(value)) {
231
+ count += 1;
232
+ }
233
+ return count;
234
+ }
205
235
  var InlineParser = class {
206
236
  constructor(options = {}) {
207
237
  this.plugins = [];
@@ -249,6 +279,42 @@ var InlineParser = class {
249
279
  }
250
280
  return result;
251
281
  }
282
+ /**
283
+ * Streaming regex anticipation helper. Returns an append string if a plugin
284
+ * declares an incomplete match at the end of the buffer.
285
+ */
286
+ getRegexAnticipationAppend(content) {
287
+ if (!content || this.plugins.length === 0) {
288
+ return null;
289
+ }
290
+ for (const plugin of this.plugins) {
291
+ if (!("re" in plugin)) continue;
292
+ const regexPlugin = plugin;
293
+ const anticipation = regexPlugin.anticipation;
294
+ if (!anticipation) continue;
295
+ const maxScanChars = Number.isFinite(anticipation.maxScanChars ?? Number.NaN) ? Math.max(1, anticipation.maxScanChars ?? 0) : 240;
296
+ const scan = content.slice(Math.max(0, content.length - maxScanChars));
297
+ if (regexPlugin.fastCheck && !regexPlugin.fastCheck(scan)) {
298
+ continue;
299
+ }
300
+ const lastStart = findLastMatch(anticipation.start, scan);
301
+ if (!lastStart) continue;
302
+ if (isSamePattern(anticipation.start, anticipation.end)) {
303
+ const occurrences = countMatches(anticipation.start, scan);
304
+ if (occurrences % 2 === 0) {
305
+ continue;
306
+ }
307
+ } else {
308
+ const startIndex = lastStart.index + lastStart[0].length;
309
+ const hasEnd = Boolean(findMatchAfter(anticipation.end, scan, startIndex));
310
+ if (hasEnd) continue;
311
+ }
312
+ const appendValue = typeof anticipation.append === "function" ? anticipation.append(lastStart, content) : anticipation.append;
313
+ if (!appendValue) continue;
314
+ return appendValue;
315
+ }
316
+ return null;
317
+ }
252
318
  /**
253
319
  * Clear the memoization cache
254
320
  */
@@ -377,9 +443,16 @@ var InlineParser = class {
377
443
  this.registerPlugin({
378
444
  id: "citations",
379
445
  priority: 10,
380
- re: /\[\^([^\]]+)\]|@cite\{([^}]+)\}/g,
381
- toNode: (match) => ({ kind: "citation", id: match[1] || match[2] }),
382
- fastCheck: (text) => text.indexOf("@") !== -1 || text.indexOf("[^") !== -1
446
+ re: /\[\^([^\]\n]+)\]|@cite\{([^}\n]+)\}|\{cite:([^}\n]+)\}/g,
447
+ toNode: (match) => ({ kind: "citation", id: match[1] || match[2] || match[3] }),
448
+ fastCheck: (text) => text.indexOf("@cite") !== -1 || text.indexOf("[^") !== -1 || text.indexOf("{cite:") !== -1,
449
+ anticipation: {
450
+ start: /@cite\{|\{cite:/g,
451
+ end: /\}/g,
452
+ full: /@cite\{[^}\n]+?\}|\{cite:[^}\n]+?\}/g,
453
+ append: "}",
454
+ maxScanChars: 120
455
+ }
383
456
  });
384
457
  this.registerPlugin({
385
458
  id: "mentions",
@@ -479,9 +552,23 @@ import * as rehypeParse from "rehype-parse";
479
552
  import * as rehypeSanitize from "rehype-sanitize";
480
553
  import * as rehypeStringify from "rehype-stringify";
481
554
  import { unified } from "unified";
482
- var { defaultSchema } = rehypeSanitize;
555
+ var rehypeSanitizeModule = rehypeSanitize;
556
+ var defaultSchema = rehypeSanitizeModule.defaultSchema;
557
+ var resolvePlugin = (mod) => {
558
+ if (typeof mod === "function") return mod;
559
+ if (mod && typeof mod.default === "function") {
560
+ return mod.default;
561
+ }
562
+ if (mod && typeof mod.default?.default === "function") {
563
+ return mod.default?.default;
564
+ }
565
+ return mod;
566
+ };
567
+ var rehypeParsePlugin = resolvePlugin(rehypeParse);
568
+ var rehypeSanitizePlugin = resolvePlugin(rehypeSanitizeModule);
569
+ var rehypeStringifyPlugin = resolvePlugin(rehypeStringify);
483
570
  var SANITIZED_SCHEMA = createSchema();
484
- var sanitizeProcessor = unified().use(rehypeParse.default, { fragment: true }).use(rehypeSanitize.default, SANITIZED_SCHEMA).use(rehypeStringify.default).freeze();
571
+ var sanitizeProcessor = unified().use(rehypeParsePlugin, { fragment: true }).use(rehypeSanitizePlugin, SANITIZED_SCHEMA).use(rehypeStringifyPlugin).freeze();
485
572
  function sanitizeHtmlInWorker(html) {
486
573
  if (!html) return "";
487
574
  try {
@@ -597,9 +684,27 @@ function mergeAttributes(existing, additions) {
597
684
  }
598
685
 
599
686
  // src/mixed-content.ts
600
- function extractMixedContentSegments(raw, baseOffset, parseInline) {
687
+ var DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS = /* @__PURE__ */ new Set([
688
+ "span",
689
+ "em",
690
+ "strong",
691
+ "code",
692
+ "kbd",
693
+ "del",
694
+ "s",
695
+ "mark",
696
+ "sub",
697
+ "sup",
698
+ "i",
699
+ "b",
700
+ "u",
701
+ "small",
702
+ "abbr",
703
+ "a"
704
+ ]);
705
+ function extractMixedContentSegments(raw, baseOffset, parseInline, options) {
601
706
  if (!raw) return [];
602
- const initial = splitByTagSegments(raw, baseOffset, parseInline);
707
+ const initial = splitByTagSegments(raw, baseOffset, parseInline, options);
603
708
  const expanded = [];
604
709
  for (const segment of initial) {
605
710
  if (segment.kind === "text") {
@@ -610,22 +715,58 @@ function extractMixedContentSegments(raw, baseOffset, parseInline) {
610
715
  }
611
716
  return mergeAdjacentTextSegments(expanded, parseInline);
612
717
  }
613
- function splitByTagSegments(source, baseOffset, parseInline) {
718
+ function splitByTagSegments(source, baseOffset, parseInline, options) {
614
719
  const segments = [];
615
720
  const lowerSource = source.toLowerCase();
616
721
  const tagPattern = /<([A-Za-z][\w:-]*)([^<>]*?)\/?>/g;
617
722
  let cursor = 0;
618
723
  let match = tagPattern.exec(source);
619
724
  const baseIsFinite = typeof baseOffset === "number" && Number.isFinite(baseOffset);
725
+ const htmlAllowTags = normalizeHtmlAllowlist(options?.html?.allowTags);
726
+ const htmlAutoClose = options?.html?.autoClose === true;
727
+ const htmlMaxNewlines = normalizeNewlineLimit(options?.html?.maxNewlines);
728
+ const mdxAutoClose = options?.mdx?.autoClose === true;
729
+ const mdxMaxNewlines = normalizeNewlineLimit(options?.mdx?.maxNewlines);
730
+ const mdxAllowlist = normalizeComponentAllowlist(options?.mdx?.componentAllowlist);
620
731
  while (match !== null) {
621
732
  const start = match.index;
622
733
  const tagName = match[1];
623
734
  const matchText = match[0];
624
- const isSelfClosing = matchText.endsWith("/>") || isVoidHtmlTag(tagName);
735
+ const tagNameLower = tagName.toLowerCase();
736
+ const isSelfClosing = matchText.endsWith("/>") || isVoidHtmlTag(tagNameLower);
737
+ const mdxCandidate = isLikelyMdxComponent(tagName);
738
+ const mdxAllowed = mdxCandidate && (!mdxAllowlist || mdxAllowlist.has(tagName));
739
+ if (mdxCandidate && mdxAllowlist && !mdxAllowed) {
740
+ tagPattern.lastIndex = start + 1;
741
+ match = tagPattern.exec(source);
742
+ continue;
743
+ }
625
744
  let end = tagPattern.lastIndex;
626
- if (!isSelfClosing && !isLikelyMdxComponent(tagName)) {
745
+ if (!isSelfClosing && !mdxAllowed) {
627
746
  const closingIndex = findClosingHtmlTag(lowerSource, tagName.toLowerCase(), end);
628
747
  if (closingIndex === -1) {
748
+ if (htmlAutoClose && htmlAllowTags.has(tagNameLower)) {
749
+ const tail = source.slice(end);
750
+ const newlineCount = countNewlines(tail, htmlMaxNewlines + 1);
751
+ if (newlineCount <= htmlMaxNewlines) {
752
+ if (start > cursor) {
753
+ const absoluteFrom = baseIsFinite ? baseOffset + cursor : void 0;
754
+ const absoluteTo = baseIsFinite ? baseOffset + start : void 0;
755
+ pushTextSegment(segments, source.slice(cursor, start), absoluteFrom, absoluteTo, parseInline);
756
+ }
757
+ const rawSegment2 = source.slice(start);
758
+ const closedValue = `${rawSegment2}</${tagName}>`;
759
+ const segment2 = {
760
+ kind: "html",
761
+ value: closedValue,
762
+ range: createSegmentRange(baseOffset, start, source.length),
763
+ sanitized: sanitizeHtmlInWorker(closedValue)
764
+ };
765
+ segments.push(segment2);
766
+ cursor = source.length;
767
+ break;
768
+ }
769
+ }
629
770
  tagPattern.lastIndex = start + 1;
630
771
  match = tagPattern.exec(source);
631
772
  continue;
@@ -637,8 +778,8 @@ function splitByTagSegments(source, baseOffset, parseInline) {
637
778
  const absoluteTo = baseIsFinite ? baseOffset + start : void 0;
638
779
  pushTextSegment(segments, source.slice(cursor, start), absoluteFrom, absoluteTo, parseInline);
639
780
  }
640
- const rawSegment = source.slice(start, end);
641
- const kind = isLikelyMdxComponent(tagName) ? "mdx" : "html";
781
+ let rawSegment = source.slice(start, end);
782
+ const kind = mdxAllowed ? "mdx" : "html";
642
783
  const segment = {
643
784
  kind,
644
785
  value: rawSegment,
@@ -647,6 +788,17 @@ function splitByTagSegments(source, baseOffset, parseInline) {
647
788
  if (kind === "html") {
648
789
  segment.sanitized = sanitizeHtmlInWorker(rawSegment);
649
790
  } else {
791
+ const tail = source.slice(end);
792
+ const newlineCount = countNewlines(tail, mdxMaxNewlines + 1);
793
+ if (mdxAutoClose && newlineCount > mdxMaxNewlines) {
794
+ tagPattern.lastIndex = start + 1;
795
+ match = tagPattern.exec(source);
796
+ continue;
797
+ }
798
+ if (mdxAutoClose && !rawSegment.endsWith("/>")) {
799
+ rawSegment = selfCloseTag(rawSegment);
800
+ segment.value = rawSegment;
801
+ }
650
802
  segment.status = "pending";
651
803
  }
652
804
  segments.push(segment);
@@ -771,6 +923,48 @@ var VOID_HTML_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "meta", "input"
771
923
  function isVoidHtmlTag(tagName) {
772
924
  return VOID_HTML_TAGS.has(tagName.toLowerCase());
773
925
  }
926
+ function normalizeNewlineLimit(value) {
927
+ if (!Number.isFinite(value ?? Number.NaN)) {
928
+ return 2;
929
+ }
930
+ return Math.max(0, value ?? 0);
931
+ }
932
+ function normalizeHtmlAllowlist(value) {
933
+ if (!value) return DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS;
934
+ const tags = /* @__PURE__ */ new Set();
935
+ for (const tag of value) {
936
+ if (tag) {
937
+ tags.add(tag.toLowerCase());
938
+ }
939
+ }
940
+ return tags.size > 0 ? tags : DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS;
941
+ }
942
+ function normalizeComponentAllowlist(value) {
943
+ if (!value) return null;
944
+ const tags = /* @__PURE__ */ new Set();
945
+ for (const tag of value) {
946
+ if (tag) tags.add(tag);
947
+ }
948
+ return tags.size > 0 ? tags : null;
949
+ }
950
+ function countNewlines(value, limit) {
951
+ let count = 0;
952
+ for (let i = 0; i < value.length; i++) {
953
+ if (value.charCodeAt(i) === 10) {
954
+ count += 1;
955
+ if (limit !== void 0 && count >= limit) {
956
+ return count;
957
+ }
958
+ }
959
+ }
960
+ return count;
961
+ }
962
+ function selfCloseTag(rawTag) {
963
+ if (rawTag.endsWith("/>")) return rawTag;
964
+ const closeIndex = rawTag.lastIndexOf(">");
965
+ if (closeIndex === -1) return rawTag;
966
+ return `${rawTag.slice(0, closeIndex)}/>`;
967
+ }
774
968
  function isLikelyMdxComponent(tagName) {
775
969
  const first = tagName.charAt(0);
776
970
  return first.toUpperCase() === first && first.toLowerCase() !== first;
@@ -2723,63 +2917,126 @@ var CustomStreamingMatcher = class {
2723
2917
  };
2724
2918
 
2725
2919
  // src/streaming/inline-streaming.ts
2920
+ var DEFAULT_FORMAT_ANTICIPATION = {
2921
+ inline: false,
2922
+ mathInline: false,
2923
+ mathBlock: false,
2924
+ html: false,
2925
+ mdx: false,
2926
+ regex: false
2927
+ };
2928
+ function normalizeFormatAnticipation(input) {
2929
+ if (input === true) {
2930
+ return { ...DEFAULT_FORMAT_ANTICIPATION, inline: true };
2931
+ }
2932
+ if (!input) {
2933
+ return { ...DEFAULT_FORMAT_ANTICIPATION };
2934
+ }
2935
+ return {
2936
+ inline: input.inline ?? false,
2937
+ mathInline: input.mathInline ?? false,
2938
+ mathBlock: input.mathBlock ?? false,
2939
+ html: input.html ?? false,
2940
+ mdx: input.mdx ?? false,
2941
+ regex: input.regex ?? false
2942
+ };
2943
+ }
2726
2944
  function prepareInlineStreamingContent(content, options) {
2727
- const enableAnticipation = Boolean(options?.formatAnticipation);
2728
2945
  const enableMath = options?.math !== false;
2729
- let dollarCount = 0;
2730
- let backtickCount = 0;
2731
- let starCount = 0;
2732
- let doubleStarCount = 0;
2733
- let tildePairCount = 0;
2946
+ const anticipation = normalizeFormatAnticipation(options?.formatAnticipation);
2947
+ const enableInlineAnticipation = anticipation.inline;
2948
+ const enableMathInlineAnticipation = anticipation.mathInline;
2949
+ const enableMathBlockAnticipation = anticipation.mathBlock;
2950
+ const stack = [];
2951
+ const toggleToken = (token) => {
2952
+ const last = stack[stack.length - 1];
2953
+ if (last === token) {
2954
+ stack.pop();
2955
+ } else {
2956
+ stack.push(token);
2957
+ }
2958
+ };
2959
+ let mathDisplayOpen = false;
2960
+ let mathDisplayCrossedNewline = false;
2734
2961
  for (let i = 0; i < content.length; i++) {
2735
2962
  const code = content.charCodeAt(i);
2736
- if (code === 36) {
2737
- dollarCount += 1;
2963
+ if (code === 10 || code === 13) {
2964
+ if (mathDisplayOpen) {
2965
+ mathDisplayCrossedNewline = true;
2966
+ }
2738
2967
  continue;
2739
2968
  }
2740
2969
  if (code === 96) {
2741
- backtickCount += 1;
2970
+ toggleToken("code");
2971
+ continue;
2972
+ }
2973
+ if (code === 126 && i + 1 < content.length && content.charCodeAt(i + 1) === 126) {
2974
+ toggleToken("strike");
2975
+ i += 1;
2742
2976
  continue;
2743
2977
  }
2744
2978
  if (code === 42) {
2745
2979
  if (i + 1 < content.length && content.charCodeAt(i + 1) === 42) {
2746
- doubleStarCount += 1;
2747
- starCount += 2;
2980
+ toggleToken("strong");
2748
2981
  i += 1;
2749
2982
  } else {
2750
- starCount += 1;
2983
+ toggleToken("em");
2751
2984
  }
2752
2985
  continue;
2753
2986
  }
2754
- if (code === 126) {
2755
- if (i + 1 < content.length && content.charCodeAt(i + 1) === 126) {
2756
- tildePairCount += 1;
2987
+ if (enableMath && code === 36) {
2988
+ if (i + 1 < content.length && content.charCodeAt(i + 1) === 36) {
2989
+ toggleToken("math-display");
2990
+ if (mathDisplayOpen) {
2991
+ mathDisplayOpen = false;
2992
+ mathDisplayCrossedNewline = false;
2993
+ } else {
2994
+ mathDisplayOpen = true;
2995
+ mathDisplayCrossedNewline = false;
2996
+ }
2757
2997
  i += 1;
2998
+ } else {
2999
+ toggleToken("math-inline");
2758
3000
  }
2759
3001
  }
2760
3002
  }
2761
- const hasIncompleteMath = enableMath && dollarCount % 2 !== 0;
2762
- if (hasIncompleteMath) {
2763
- return { kind: "raw", status: "raw", reason: "incomplete-math" };
2764
- }
2765
- const hasIncompleteCode = backtickCount % 2 !== 0;
2766
- const hasIncompleteStrong = doubleStarCount % 2 !== 0;
2767
- const singleStarCount = starCount - doubleStarCount * 2;
2768
- const hasIncompleteEmphasis = singleStarCount % 2 !== 0;
2769
- const hasIncompleteStrike = tildePairCount % 2 !== 0;
2770
- const hasAnyIncomplete = hasIncompleteCode || hasIncompleteStrong || hasIncompleteEmphasis || hasIncompleteStrike;
2771
- if (!hasAnyIncomplete) {
2772
- return { kind: "parse", status: "complete", content, appended: "" };
3003
+ const hasIncompleteFormatting = stack.some((token) => token === "code" || token === "strike" || token === "strong" || token === "em");
3004
+ const hasIncompleteMathInline = stack.includes("math-inline");
3005
+ const hasIncompleteMathDisplay = stack.includes("math-display");
3006
+ const hasIncompleteMath = hasIncompleteMathInline || hasIncompleteMathDisplay;
3007
+ if (enableMath && hasIncompleteMath) {
3008
+ if (hasIncompleteMathInline && !enableMathInlineAnticipation) {
3009
+ return { kind: "raw", status: "raw", reason: "incomplete-math" };
3010
+ }
3011
+ if (hasIncompleteMathDisplay && (!enableMathBlockAnticipation || mathDisplayCrossedNewline)) {
3012
+ return { kind: "raw", status: "raw", reason: "incomplete-math" };
3013
+ }
2773
3014
  }
2774
- if (!enableAnticipation) {
3015
+ if (hasIncompleteFormatting && !enableInlineAnticipation) {
2775
3016
  return { kind: "raw", status: "raw", reason: "incomplete-formatting" };
2776
3017
  }
2777
- let appended = "";
2778
- if (hasIncompleteCode) appended += "`";
2779
- if (hasIncompleteStrike) appended += "~~";
2780
- if (hasIncompleteStrong && hasIncompleteEmphasis) appended += "***";
2781
- else if (hasIncompleteStrong) appended += "**";
2782
- else if (hasIncompleteEmphasis) appended += "*";
3018
+ if (!hasIncompleteFormatting && !hasIncompleteMath) {
3019
+ return { kind: "parse", status: "complete", content, appended: "" };
3020
+ }
3021
+ const appendForToken = (token) => {
3022
+ switch (token) {
3023
+ case "code":
3024
+ return "`";
3025
+ case "strike":
3026
+ return "~~";
3027
+ case "strong":
3028
+ return "**";
3029
+ case "em":
3030
+ return "*";
3031
+ case "math-inline":
3032
+ return "$";
3033
+ case "math-display":
3034
+ return "$$";
3035
+ default:
3036
+ return "";
3037
+ }
3038
+ };
3039
+ const appended = stack.slice().reverse().map((token) => appendForToken(token)).join("");
2783
3040
  return { kind: "parse", status: "anticipated", content: content + appended, appended };
2784
3041
  }
2785
3042
  export {
@@ -2825,6 +3082,7 @@ export {
2825
3082
  inlineNodesToPlainText,
2826
3083
  isLikelyMdxComponent,
2827
3084
  normalizeBlockquoteText,
3085
+ normalizeFormatAnticipation,
2828
3086
  normalizeLang,
2829
3087
  parseCodeFenceInfo,
2830
3088
  prepareInlineStreamingContent,
@@ -25,6 +25,36 @@ __export(inline_parser_exports, {
25
25
  applyRegexPlugin: () => applyRegexPlugin
26
26
  });
27
27
  module.exports = __toCommonJS(inline_parser_exports);
28
+ function ensureGlobal(pattern) {
29
+ if (pattern.global) return pattern;
30
+ const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
31
+ return new RegExp(pattern.source, flags);
32
+ }
33
+ function findLastMatch(pattern, value) {
34
+ const re = ensureGlobal(pattern);
35
+ let match = null;
36
+ let next;
37
+ while ((next = re.exec(value)) !== null) {
38
+ match = next;
39
+ }
40
+ return match;
41
+ }
42
+ function findMatchAfter(pattern, value, startIndex) {
43
+ const re = ensureGlobal(pattern);
44
+ re.lastIndex = Math.max(0, startIndex);
45
+ return re.exec(value);
46
+ }
47
+ function isSamePattern(a, b) {
48
+ return a.source === b.source && a.flags === b.flags;
49
+ }
50
+ function countMatches(pattern, value) {
51
+ const re = ensureGlobal(pattern);
52
+ let count = 0;
53
+ while (re.exec(value)) {
54
+ count += 1;
55
+ }
56
+ return count;
57
+ }
28
58
  var InlineParser = class {
29
59
  constructor(options = {}) {
30
60
  this.plugins = [];
@@ -72,6 +102,42 @@ var InlineParser = class {
72
102
  }
73
103
  return result;
74
104
  }
105
+ /**
106
+ * Streaming regex anticipation helper. Returns an append string if a plugin
107
+ * declares an incomplete match at the end of the buffer.
108
+ */
109
+ getRegexAnticipationAppend(content) {
110
+ if (!content || this.plugins.length === 0) {
111
+ return null;
112
+ }
113
+ for (const plugin of this.plugins) {
114
+ if (!("re" in plugin)) continue;
115
+ const regexPlugin = plugin;
116
+ const anticipation = regexPlugin.anticipation;
117
+ if (!anticipation) continue;
118
+ const maxScanChars = Number.isFinite(anticipation.maxScanChars ?? Number.NaN) ? Math.max(1, anticipation.maxScanChars ?? 0) : 240;
119
+ const scan = content.slice(Math.max(0, content.length - maxScanChars));
120
+ if (regexPlugin.fastCheck && !regexPlugin.fastCheck(scan)) {
121
+ continue;
122
+ }
123
+ const lastStart = findLastMatch(anticipation.start, scan);
124
+ if (!lastStart) continue;
125
+ if (isSamePattern(anticipation.start, anticipation.end)) {
126
+ const occurrences = countMatches(anticipation.start, scan);
127
+ if (occurrences % 2 === 0) {
128
+ continue;
129
+ }
130
+ } else {
131
+ const startIndex = lastStart.index + lastStart[0].length;
132
+ const hasEnd = Boolean(findMatchAfter(anticipation.end, scan, startIndex));
133
+ if (hasEnd) continue;
134
+ }
135
+ const appendValue = typeof anticipation.append === "function" ? anticipation.append(lastStart, content) : anticipation.append;
136
+ if (!appendValue) continue;
137
+ return appendValue;
138
+ }
139
+ return null;
140
+ }
75
141
  /**
76
142
  * Clear the memoization cache
77
143
  */
@@ -200,9 +266,16 @@ var InlineParser = class {
200
266
  this.registerPlugin({
201
267
  id: "citations",
202
268
  priority: 10,
203
- re: /\[\^([^\]]+)\]|@cite\{([^}]+)\}/g,
204
- toNode: (match) => ({ kind: "citation", id: match[1] || match[2] }),
205
- fastCheck: (text) => text.indexOf("@") !== -1 || text.indexOf("[^") !== -1
269
+ re: /\[\^([^\]\n]+)\]|@cite\{([^}\n]+)\}|\{cite:([^}\n]+)\}/g,
270
+ toNode: (match) => ({ kind: "citation", id: match[1] || match[2] || match[3] }),
271
+ fastCheck: (text) => text.indexOf("@cite") !== -1 || text.indexOf("[^") !== -1 || text.indexOf("{cite:") !== -1,
272
+ anticipation: {
273
+ start: /@cite\{|\{cite:/g,
274
+ end: /\}/g,
275
+ full: /@cite\{[^}\n]+?\}|\{cite:[^}\n]+?\}/g,
276
+ append: "}",
277
+ maxScanChars: 120
278
+ }
206
279
  });
207
280
  this.registerPlugin({
208
281
  id: "mentions",
@@ -36,6 +36,11 @@ declare class InlineParser {
36
36
  * Parse inline content with memoization
37
37
  */
38
38
  parse(content: string, options?: InlineParseOptions): InlineNode[];
39
+ /**
40
+ * Streaming regex anticipation helper. Returns an append string if a plugin
41
+ * declares an incomplete match at the end of the buffer.
42
+ */
43
+ getRegexAnticipationAppend(content: string): string | null;
39
44
  /**
40
45
  * Clear the memoization cache
41
46
  */
@@ -36,6 +36,11 @@ declare class InlineParser {
36
36
  * Parse inline content with memoization
37
37
  */
38
38
  parse(content: string, options?: InlineParseOptions): InlineNode[];
39
+ /**
40
+ * Streaming regex anticipation helper. Returns an append string if a plugin
41
+ * declares an incomplete match at the end of the buffer.
42
+ */
43
+ getRegexAnticipationAppend(content: string): string | null;
39
44
  /**
40
45
  * Clear the memoization cache
41
46
  */
@@ -1,4 +1,34 @@
1
1
  // src/inline-parser.ts
2
+ function ensureGlobal(pattern) {
3
+ if (pattern.global) return pattern;
4
+ const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
5
+ return new RegExp(pattern.source, flags);
6
+ }
7
+ function findLastMatch(pattern, value) {
8
+ const re = ensureGlobal(pattern);
9
+ let match = null;
10
+ let next;
11
+ while ((next = re.exec(value)) !== null) {
12
+ match = next;
13
+ }
14
+ return match;
15
+ }
16
+ function findMatchAfter(pattern, value, startIndex) {
17
+ const re = ensureGlobal(pattern);
18
+ re.lastIndex = Math.max(0, startIndex);
19
+ return re.exec(value);
20
+ }
21
+ function isSamePattern(a, b) {
22
+ return a.source === b.source && a.flags === b.flags;
23
+ }
24
+ function countMatches(pattern, value) {
25
+ const re = ensureGlobal(pattern);
26
+ let count = 0;
27
+ while (re.exec(value)) {
28
+ count += 1;
29
+ }
30
+ return count;
31
+ }
2
32
  var InlineParser = class {
3
33
  constructor(options = {}) {
4
34
  this.plugins = [];
@@ -46,6 +76,42 @@ var InlineParser = class {
46
76
  }
47
77
  return result;
48
78
  }
79
+ /**
80
+ * Streaming regex anticipation helper. Returns an append string if a plugin
81
+ * declares an incomplete match at the end of the buffer.
82
+ */
83
+ getRegexAnticipationAppend(content) {
84
+ if (!content || this.plugins.length === 0) {
85
+ return null;
86
+ }
87
+ for (const plugin of this.plugins) {
88
+ if (!("re" in plugin)) continue;
89
+ const regexPlugin = plugin;
90
+ const anticipation = regexPlugin.anticipation;
91
+ if (!anticipation) continue;
92
+ const maxScanChars = Number.isFinite(anticipation.maxScanChars ?? Number.NaN) ? Math.max(1, anticipation.maxScanChars ?? 0) : 240;
93
+ const scan = content.slice(Math.max(0, content.length - maxScanChars));
94
+ if (regexPlugin.fastCheck && !regexPlugin.fastCheck(scan)) {
95
+ continue;
96
+ }
97
+ const lastStart = findLastMatch(anticipation.start, scan);
98
+ if (!lastStart) continue;
99
+ if (isSamePattern(anticipation.start, anticipation.end)) {
100
+ const occurrences = countMatches(anticipation.start, scan);
101
+ if (occurrences % 2 === 0) {
102
+ continue;
103
+ }
104
+ } else {
105
+ const startIndex = lastStart.index + lastStart[0].length;
106
+ const hasEnd = Boolean(findMatchAfter(anticipation.end, scan, startIndex));
107
+ if (hasEnd) continue;
108
+ }
109
+ const appendValue = typeof anticipation.append === "function" ? anticipation.append(lastStart, content) : anticipation.append;
110
+ if (!appendValue) continue;
111
+ return appendValue;
112
+ }
113
+ return null;
114
+ }
49
115
  /**
50
116
  * Clear the memoization cache
51
117
  */
@@ -174,9 +240,16 @@ var InlineParser = class {
174
240
  this.registerPlugin({
175
241
  id: "citations",
176
242
  priority: 10,
177
- re: /\[\^([^\]]+)\]|@cite\{([^}]+)\}/g,
178
- toNode: (match) => ({ kind: "citation", id: match[1] || match[2] }),
179
- fastCheck: (text) => text.indexOf("@") !== -1 || text.indexOf("[^") !== -1
243
+ re: /\[\^([^\]\n]+)\]|@cite\{([^}\n]+)\}|\{cite:([^}\n]+)\}/g,
244
+ toNode: (match) => ({ kind: "citation", id: match[1] || match[2] || match[3] }),
245
+ fastCheck: (text) => text.indexOf("@cite") !== -1 || text.indexOf("[^") !== -1 || text.indexOf("{cite:") !== -1,
246
+ anticipation: {
247
+ start: /@cite\{|\{cite:/g,
248
+ end: /\}/g,
249
+ full: /@cite\{[^}\n]+?\}|\{cite:[^}\n]+?\}/g,
250
+ append: "}",
251
+ maxScanChars: 120
252
+ }
180
253
  });
181
254
  this.registerPlugin({
182
255
  id: "mentions",