@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.
@@ -1,63 +1,127 @@
1
1
  // src/streaming/inline-streaming.ts
2
+ var DEFAULT_FORMAT_ANTICIPATION = {
3
+ inline: false,
4
+ mathInline: false,
5
+ mathBlock: false,
6
+ html: false,
7
+ mdx: false,
8
+ regex: false
9
+ };
10
+ function normalizeFormatAnticipation(input) {
11
+ if (input === true) {
12
+ return { ...DEFAULT_FORMAT_ANTICIPATION, inline: true };
13
+ }
14
+ if (!input) {
15
+ return { ...DEFAULT_FORMAT_ANTICIPATION };
16
+ }
17
+ return {
18
+ inline: input.inline ?? false,
19
+ mathInline: input.mathInline ?? false,
20
+ mathBlock: input.mathBlock ?? false,
21
+ html: input.html ?? false,
22
+ mdx: input.mdx ?? false,
23
+ regex: input.regex ?? false
24
+ };
25
+ }
2
26
  function prepareInlineStreamingContent(content, options) {
3
- const enableAnticipation = Boolean(options?.formatAnticipation);
4
27
  const enableMath = options?.math !== false;
5
- let dollarCount = 0;
6
- let backtickCount = 0;
7
- let starCount = 0;
8
- let doubleStarCount = 0;
9
- let tildePairCount = 0;
28
+ const anticipation = normalizeFormatAnticipation(options?.formatAnticipation);
29
+ const enableInlineAnticipation = anticipation.inline;
30
+ const enableMathInlineAnticipation = anticipation.mathInline;
31
+ const enableMathBlockAnticipation = anticipation.mathBlock;
32
+ const stack = [];
33
+ const toggleToken = (token) => {
34
+ const last = stack[stack.length - 1];
35
+ if (last === token) {
36
+ stack.pop();
37
+ } else {
38
+ stack.push(token);
39
+ }
40
+ };
41
+ let mathDisplayOpen = false;
42
+ let mathDisplayCrossedNewline = false;
10
43
  for (let i = 0; i < content.length; i++) {
11
44
  const code = content.charCodeAt(i);
12
- if (code === 36) {
13
- dollarCount += 1;
45
+ if (code === 10 || code === 13) {
46
+ if (mathDisplayOpen) {
47
+ mathDisplayCrossedNewline = true;
48
+ }
14
49
  continue;
15
50
  }
16
51
  if (code === 96) {
17
- backtickCount += 1;
52
+ toggleToken("code");
53
+ continue;
54
+ }
55
+ if (code === 126 && i + 1 < content.length && content.charCodeAt(i + 1) === 126) {
56
+ toggleToken("strike");
57
+ i += 1;
18
58
  continue;
19
59
  }
20
60
  if (code === 42) {
21
61
  if (i + 1 < content.length && content.charCodeAt(i + 1) === 42) {
22
- doubleStarCount += 1;
23
- starCount += 2;
62
+ toggleToken("strong");
24
63
  i += 1;
25
64
  } else {
26
- starCount += 1;
65
+ toggleToken("em");
27
66
  }
28
67
  continue;
29
68
  }
30
- if (code === 126) {
31
- if (i + 1 < content.length && content.charCodeAt(i + 1) === 126) {
32
- tildePairCount += 1;
69
+ if (enableMath && code === 36) {
70
+ if (i + 1 < content.length && content.charCodeAt(i + 1) === 36) {
71
+ toggleToken("math-display");
72
+ if (mathDisplayOpen) {
73
+ mathDisplayOpen = false;
74
+ mathDisplayCrossedNewline = false;
75
+ } else {
76
+ mathDisplayOpen = true;
77
+ mathDisplayCrossedNewline = false;
78
+ }
33
79
  i += 1;
80
+ } else {
81
+ toggleToken("math-inline");
34
82
  }
35
83
  }
36
84
  }
37
- const hasIncompleteMath = enableMath && dollarCount % 2 !== 0;
38
- if (hasIncompleteMath) {
39
- return { kind: "raw", status: "raw", reason: "incomplete-math" };
40
- }
41
- const hasIncompleteCode = backtickCount % 2 !== 0;
42
- const hasIncompleteStrong = doubleStarCount % 2 !== 0;
43
- const singleStarCount = starCount - doubleStarCount * 2;
44
- const hasIncompleteEmphasis = singleStarCount % 2 !== 0;
45
- const hasIncompleteStrike = tildePairCount % 2 !== 0;
46
- const hasAnyIncomplete = hasIncompleteCode || hasIncompleteStrong || hasIncompleteEmphasis || hasIncompleteStrike;
47
- if (!hasAnyIncomplete) {
48
- return { kind: "parse", status: "complete", content, appended: "" };
85
+ const hasIncompleteFormatting = stack.some((token) => token === "code" || token === "strike" || token === "strong" || token === "em");
86
+ const hasIncompleteMathInline = stack.includes("math-inline");
87
+ const hasIncompleteMathDisplay = stack.includes("math-display");
88
+ const hasIncompleteMath = hasIncompleteMathInline || hasIncompleteMathDisplay;
89
+ if (enableMath && hasIncompleteMath) {
90
+ if (hasIncompleteMathInline && !enableMathInlineAnticipation) {
91
+ return { kind: "raw", status: "raw", reason: "incomplete-math" };
92
+ }
93
+ if (hasIncompleteMathDisplay && (!enableMathBlockAnticipation || mathDisplayCrossedNewline)) {
94
+ return { kind: "raw", status: "raw", reason: "incomplete-math" };
95
+ }
49
96
  }
50
- if (!enableAnticipation) {
97
+ if (hasIncompleteFormatting && !enableInlineAnticipation) {
51
98
  return { kind: "raw", status: "raw", reason: "incomplete-formatting" };
52
99
  }
53
- let appended = "";
54
- if (hasIncompleteCode) appended += "`";
55
- if (hasIncompleteStrike) appended += "~~";
56
- if (hasIncompleteStrong && hasIncompleteEmphasis) appended += "***";
57
- else if (hasIncompleteStrong) appended += "**";
58
- else if (hasIncompleteEmphasis) appended += "*";
100
+ if (!hasIncompleteFormatting && !hasIncompleteMath) {
101
+ return { kind: "parse", status: "complete", content, appended: "" };
102
+ }
103
+ const appendForToken = (token) => {
104
+ switch (token) {
105
+ case "code":
106
+ return "`";
107
+ case "strike":
108
+ return "~~";
109
+ case "strong":
110
+ return "**";
111
+ case "em":
112
+ return "*";
113
+ case "math-inline":
114
+ return "$";
115
+ case "math-display":
116
+ return "$$";
117
+ default:
118
+ return "";
119
+ }
120
+ };
121
+ const appended = stack.slice().reverse().map((token) => appendForToken(token)).join("");
59
122
  return { kind: "parse", status: "anticipated", content: content + appended, appended };
60
123
  }
61
124
  export {
125
+ normalizeFormatAnticipation,
62
126
  prepareInlineStreamingContent
63
127
  };
package/dist/types.d.cts CHANGED
@@ -39,6 +39,14 @@ interface MixedContentSegment {
39
39
  status?: "pending" | "compiled" | "error";
40
40
  error?: string;
41
41
  }
42
+ type FormatAnticipationConfig = boolean | {
43
+ inline?: boolean;
44
+ mathInline?: boolean;
45
+ mathBlock?: boolean;
46
+ html?: boolean;
47
+ mdx?: boolean;
48
+ regex?: boolean;
49
+ };
42
50
  interface InlineHtmlDescriptor {
43
51
  tagName: string;
44
52
  attributes: Record<string, string>;
@@ -115,8 +123,9 @@ type WorkerIn = {
115
123
  tables?: boolean;
116
124
  callouts?: boolean;
117
125
  math?: boolean;
118
- formatAnticipation?: boolean;
126
+ formatAnticipation?: FormatAnticipationConfig;
119
127
  liveCodeHighlighting?: boolean;
128
+ mdxComponentNames?: string[];
120
129
  };
121
130
  mdx?: {
122
131
  compileMode?: "server" | "worker";
@@ -126,6 +135,8 @@ type WorkerIn = {
126
135
  text: string;
127
136
  } | {
128
137
  type: "FINALIZE";
138
+ } | {
139
+ type: "DEBUG_STATE";
129
140
  } | {
130
141
  type: "MDX_COMPILED";
131
142
  blockId: string;
@@ -158,6 +169,23 @@ type WorkerOut = {
158
169
  } | {
159
170
  type: "METRICS";
160
171
  metrics: PerformanceMetrics;
172
+ } | {
173
+ type: "DEBUG_STATE";
174
+ state: {
175
+ contentLength: number;
176
+ contentTail: string;
177
+ blockCount: number;
178
+ blockTypeCounts: Record<string, number>;
179
+ lastBlockType?: string;
180
+ lastBlockRange?: {
181
+ from: number;
182
+ to: number;
183
+ };
184
+ lastBlockRawTail?: string;
185
+ hasInlineCodeHeading: boolean;
186
+ hasCodeBlocksHeading: boolean;
187
+ hasMediaHeading: boolean;
188
+ };
161
189
  } | {
162
190
  type: "ERROR";
163
191
  phase: WorkerPhase;
@@ -182,6 +210,17 @@ interface RegexInlinePlugin extends InlinePlugin {
182
210
  * the regex is skipped for that node.
183
211
  */
184
212
  fastCheck?: (text: string) => boolean;
213
+ /**
214
+ * Optional streaming anticipation config. Only used when formatAnticipation.regex is enabled.
215
+ */
216
+ anticipation?: RegexAnticipationPattern;
217
+ }
218
+ interface RegexAnticipationPattern {
219
+ start: RegExp;
220
+ end: RegExp;
221
+ full?: RegExp;
222
+ append: string | ((match: RegExpExecArray, content: string) => string);
223
+ maxScanChars?: number;
185
224
  }
186
225
  interface ASTInlinePlugin extends InlinePlugin {
187
226
  visit: (node: InlineNode, ctx: {
@@ -314,4 +353,4 @@ interface CoalescingMetrics {
314
353
  insertChildCoalesced: number;
315
354
  }
316
355
 
317
- export { type ASTInlinePlugin, type Block, type CoalescingMetrics, type CompiledMdxModule, type InlineHtmlDescriptor, type InlineNode, type InlinePlugin, LANGUAGE_ALIASES, type MixedContentSegment, type NodePath, type NodeSnapshot, PATCH_ROOT_ID, type Patch, type PatchMetrics, type PerformanceMetrics, type ProtectedRange, type ProtectedRangeKind, type RegexInlinePlugin, type SetPropsBatchEntry, type WorkerErrorPayload, type WorkerIn, type WorkerOut, type WorkerPhase };
356
+ export { type ASTInlinePlugin, type Block, type CoalescingMetrics, type CompiledMdxModule, type FormatAnticipationConfig, type InlineHtmlDescriptor, type InlineNode, type InlinePlugin, LANGUAGE_ALIASES, type MixedContentSegment, type NodePath, type NodeSnapshot, PATCH_ROOT_ID, type Patch, type PatchMetrics, type PerformanceMetrics, type ProtectedRange, type ProtectedRangeKind, type RegexAnticipationPattern, type RegexInlinePlugin, type SetPropsBatchEntry, type WorkerErrorPayload, type WorkerIn, type WorkerOut, type WorkerPhase };
package/dist/types.d.ts CHANGED
@@ -39,6 +39,14 @@ interface MixedContentSegment {
39
39
  status?: "pending" | "compiled" | "error";
40
40
  error?: string;
41
41
  }
42
+ type FormatAnticipationConfig = boolean | {
43
+ inline?: boolean;
44
+ mathInline?: boolean;
45
+ mathBlock?: boolean;
46
+ html?: boolean;
47
+ mdx?: boolean;
48
+ regex?: boolean;
49
+ };
42
50
  interface InlineHtmlDescriptor {
43
51
  tagName: string;
44
52
  attributes: Record<string, string>;
@@ -115,8 +123,9 @@ type WorkerIn = {
115
123
  tables?: boolean;
116
124
  callouts?: boolean;
117
125
  math?: boolean;
118
- formatAnticipation?: boolean;
126
+ formatAnticipation?: FormatAnticipationConfig;
119
127
  liveCodeHighlighting?: boolean;
128
+ mdxComponentNames?: string[];
120
129
  };
121
130
  mdx?: {
122
131
  compileMode?: "server" | "worker";
@@ -126,6 +135,8 @@ type WorkerIn = {
126
135
  text: string;
127
136
  } | {
128
137
  type: "FINALIZE";
138
+ } | {
139
+ type: "DEBUG_STATE";
129
140
  } | {
130
141
  type: "MDX_COMPILED";
131
142
  blockId: string;
@@ -158,6 +169,23 @@ type WorkerOut = {
158
169
  } | {
159
170
  type: "METRICS";
160
171
  metrics: PerformanceMetrics;
172
+ } | {
173
+ type: "DEBUG_STATE";
174
+ state: {
175
+ contentLength: number;
176
+ contentTail: string;
177
+ blockCount: number;
178
+ blockTypeCounts: Record<string, number>;
179
+ lastBlockType?: string;
180
+ lastBlockRange?: {
181
+ from: number;
182
+ to: number;
183
+ };
184
+ lastBlockRawTail?: string;
185
+ hasInlineCodeHeading: boolean;
186
+ hasCodeBlocksHeading: boolean;
187
+ hasMediaHeading: boolean;
188
+ };
161
189
  } | {
162
190
  type: "ERROR";
163
191
  phase: WorkerPhase;
@@ -182,6 +210,17 @@ interface RegexInlinePlugin extends InlinePlugin {
182
210
  * the regex is skipped for that node.
183
211
  */
184
212
  fastCheck?: (text: string) => boolean;
213
+ /**
214
+ * Optional streaming anticipation config. Only used when formatAnticipation.regex is enabled.
215
+ */
216
+ anticipation?: RegexAnticipationPattern;
217
+ }
218
+ interface RegexAnticipationPattern {
219
+ start: RegExp;
220
+ end: RegExp;
221
+ full?: RegExp;
222
+ append: string | ((match: RegExpExecArray, content: string) => string);
223
+ maxScanChars?: number;
185
224
  }
186
225
  interface ASTInlinePlugin extends InlinePlugin {
187
226
  visit: (node: InlineNode, ctx: {
@@ -314,4 +353,4 @@ interface CoalescingMetrics {
314
353
  insertChildCoalesced: number;
315
354
  }
316
355
 
317
- export { type ASTInlinePlugin, type Block, type CoalescingMetrics, type CompiledMdxModule, type InlineHtmlDescriptor, type InlineNode, type InlinePlugin, LANGUAGE_ALIASES, type MixedContentSegment, type NodePath, type NodeSnapshot, PATCH_ROOT_ID, type Patch, type PatchMetrics, type PerformanceMetrics, type ProtectedRange, type ProtectedRangeKind, type RegexInlinePlugin, type SetPropsBatchEntry, type WorkerErrorPayload, type WorkerIn, type WorkerOut, type WorkerPhase };
356
+ export { type ASTInlinePlugin, type Block, type CoalescingMetrics, type CompiledMdxModule, type FormatAnticipationConfig, type InlineHtmlDescriptor, type InlineNode, type InlinePlugin, LANGUAGE_ALIASES, type MixedContentSegment, type NodePath, type NodeSnapshot, PATCH_ROOT_ID, type Patch, type PatchMetrics, type PerformanceMetrics, type ProtectedRange, type ProtectedRangeKind, type RegexAnticipationPattern, type RegexInlinePlugin, type SetPropsBatchEntry, type WorkerErrorPayload, type WorkerIn, type WorkerOut, type WorkerPhase };
@@ -37,9 +37,23 @@ var rehypeParse = __toESM(require("rehype-parse"), 1);
37
37
  var rehypeSanitize = __toESM(require("rehype-sanitize"), 1);
38
38
  var rehypeStringify = __toESM(require("rehype-stringify"), 1);
39
39
  var import_unified = require("unified");
40
- var { defaultSchema } = rehypeSanitize;
40
+ var rehypeSanitizeModule = rehypeSanitize;
41
+ var defaultSchema = rehypeSanitizeModule.defaultSchema;
42
+ var resolvePlugin = (mod) => {
43
+ if (typeof mod === "function") return mod;
44
+ if (mod && typeof mod.default === "function") {
45
+ return mod.default;
46
+ }
47
+ if (mod && typeof mod.default?.default === "function") {
48
+ return mod.default?.default;
49
+ }
50
+ return mod;
51
+ };
52
+ var rehypeParsePlugin = resolvePlugin(rehypeParse);
53
+ var rehypeSanitizePlugin = resolvePlugin(rehypeSanitizeModule);
54
+ var rehypeStringifyPlugin = resolvePlugin(rehypeStringify);
41
55
  var SANITIZED_SCHEMA = createSchema();
42
- var sanitizeProcessor = (0, import_unified.unified)().use(rehypeParse.default, { fragment: true }).use(rehypeSanitize.default, SANITIZED_SCHEMA).use(rehypeStringify.default).freeze();
56
+ var sanitizeProcessor = (0, import_unified.unified)().use(rehypeParsePlugin, { fragment: true }).use(rehypeSanitizePlugin, SANITIZED_SCHEMA).use(rehypeStringifyPlugin).freeze();
43
57
  function sanitizeHtmlInWorker(html) {
44
58
  if (!html) return "";
45
59
  try {
@@ -3,9 +3,23 @@ import * as rehypeParse from "rehype-parse";
3
3
  import * as rehypeSanitize from "rehype-sanitize";
4
4
  import * as rehypeStringify from "rehype-stringify";
5
5
  import { unified } from "unified";
6
- var { defaultSchema } = rehypeSanitize;
6
+ var rehypeSanitizeModule = rehypeSanitize;
7
+ var defaultSchema = rehypeSanitizeModule.defaultSchema;
8
+ var resolvePlugin = (mod) => {
9
+ if (typeof mod === "function") return mod;
10
+ if (mod && typeof mod.default === "function") {
11
+ return mod.default;
12
+ }
13
+ if (mod && typeof mod.default?.default === "function") {
14
+ return mod.default?.default;
15
+ }
16
+ return mod;
17
+ };
18
+ var rehypeParsePlugin = resolvePlugin(rehypeParse);
19
+ var rehypeSanitizePlugin = resolvePlugin(rehypeSanitizeModule);
20
+ var rehypeStringifyPlugin = resolvePlugin(rehypeStringify);
7
21
  var SANITIZED_SCHEMA = createSchema();
8
- var sanitizeProcessor = unified().use(rehypeParse.default, { fragment: true }).use(rehypeSanitize.default, SANITIZED_SCHEMA).use(rehypeStringify.default).freeze();
22
+ var sanitizeProcessor = unified().use(rehypeParsePlugin, { fragment: true }).use(rehypeSanitizePlugin, SANITIZED_SCHEMA).use(rehypeStringifyPlugin).freeze();
9
23
  function sanitizeHtmlInWorker(html) {
10
24
  if (!html) return "";
11
25
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-mdx/core",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "description": "Core types, snapshot utilities, and perf helpers for the Streaming Markdown V2 stack",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -90,7 +90,7 @@
90
90
  "sideEffects": false,
91
91
  "scripts": {
92
92
  "build": "tsup",
93
- "test": "tsx __tests__/*.test.ts",
93
+ "test": "tsx ../../scripts/run-tests.ts __tests__",
94
94
  "clean": "rm -rf dist",
95
95
  "prepack": "npm run build"
96
96
  },