@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.
@@ -41,9 +41,23 @@ var rehypeParse = __toESM(require("rehype-parse"), 1);
41
41
  var rehypeSanitize = __toESM(require("rehype-sanitize"), 1);
42
42
  var rehypeStringify = __toESM(require("rehype-stringify"), 1);
43
43
  var import_unified = require("unified");
44
- var { defaultSchema } = rehypeSanitize;
44
+ var rehypeSanitizeModule = rehypeSanitize;
45
+ var defaultSchema = rehypeSanitizeModule.defaultSchema;
46
+ var resolvePlugin = (mod) => {
47
+ if (typeof mod === "function") return mod;
48
+ if (mod && typeof mod.default === "function") {
49
+ return mod.default;
50
+ }
51
+ if (mod && typeof mod.default?.default === "function") {
52
+ return mod.default?.default;
53
+ }
54
+ return mod;
55
+ };
56
+ var rehypeParsePlugin = resolvePlugin(rehypeParse);
57
+ var rehypeSanitizePlugin = resolvePlugin(rehypeSanitizeModule);
58
+ var rehypeStringifyPlugin = resolvePlugin(rehypeStringify);
45
59
  var SANITIZED_SCHEMA = createSchema();
46
- var sanitizeProcessor = (0, import_unified.unified)().use(rehypeParse.default, { fragment: true }).use(rehypeSanitize.default, SANITIZED_SCHEMA).use(rehypeStringify.default).freeze();
60
+ var sanitizeProcessor = (0, import_unified.unified)().use(rehypeParsePlugin, { fragment: true }).use(rehypeSanitizePlugin, SANITIZED_SCHEMA).use(rehypeStringifyPlugin).freeze();
47
61
  function sanitizeHtmlInWorker(html) {
48
62
  if (!html) return "";
49
63
  try {
@@ -159,9 +173,27 @@ function mergeAttributes(existing, additions) {
159
173
  }
160
174
 
161
175
  // src/mixed-content.ts
162
- function extractMixedContentSegments(raw, baseOffset, parseInline) {
176
+ var DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS = /* @__PURE__ */ new Set([
177
+ "span",
178
+ "em",
179
+ "strong",
180
+ "code",
181
+ "kbd",
182
+ "del",
183
+ "s",
184
+ "mark",
185
+ "sub",
186
+ "sup",
187
+ "i",
188
+ "b",
189
+ "u",
190
+ "small",
191
+ "abbr",
192
+ "a"
193
+ ]);
194
+ function extractMixedContentSegments(raw, baseOffset, parseInline, options) {
163
195
  if (!raw) return [];
164
- const initial = splitByTagSegments(raw, baseOffset, parseInline);
196
+ const initial = splitByTagSegments(raw, baseOffset, parseInline, options);
165
197
  const expanded = [];
166
198
  for (const segment of initial) {
167
199
  if (segment.kind === "text") {
@@ -172,22 +204,58 @@ function extractMixedContentSegments(raw, baseOffset, parseInline) {
172
204
  }
173
205
  return mergeAdjacentTextSegments(expanded, parseInline);
174
206
  }
175
- function splitByTagSegments(source, baseOffset, parseInline) {
207
+ function splitByTagSegments(source, baseOffset, parseInline, options) {
176
208
  const segments = [];
177
209
  const lowerSource = source.toLowerCase();
178
210
  const tagPattern = /<([A-Za-z][\w:-]*)([^<>]*?)\/?>/g;
179
211
  let cursor = 0;
180
212
  let match = tagPattern.exec(source);
181
213
  const baseIsFinite = typeof baseOffset === "number" && Number.isFinite(baseOffset);
214
+ const htmlAllowTags = normalizeHtmlAllowlist(options?.html?.allowTags);
215
+ const htmlAutoClose = options?.html?.autoClose === true;
216
+ const htmlMaxNewlines = normalizeNewlineLimit(options?.html?.maxNewlines);
217
+ const mdxAutoClose = options?.mdx?.autoClose === true;
218
+ const mdxMaxNewlines = normalizeNewlineLimit(options?.mdx?.maxNewlines);
219
+ const mdxAllowlist = normalizeComponentAllowlist(options?.mdx?.componentAllowlist);
182
220
  while (match !== null) {
183
221
  const start = match.index;
184
222
  const tagName = match[1];
185
223
  const matchText = match[0];
186
- const isSelfClosing = matchText.endsWith("/>") || isVoidHtmlTag(tagName);
224
+ const tagNameLower = tagName.toLowerCase();
225
+ const isSelfClosing = matchText.endsWith("/>") || isVoidHtmlTag(tagNameLower);
226
+ const mdxCandidate = isLikelyMdxComponent(tagName);
227
+ const mdxAllowed = mdxCandidate && (!mdxAllowlist || mdxAllowlist.has(tagName));
228
+ if (mdxCandidate && mdxAllowlist && !mdxAllowed) {
229
+ tagPattern.lastIndex = start + 1;
230
+ match = tagPattern.exec(source);
231
+ continue;
232
+ }
187
233
  let end = tagPattern.lastIndex;
188
- if (!isSelfClosing && !isLikelyMdxComponent(tagName)) {
234
+ if (!isSelfClosing && !mdxAllowed) {
189
235
  const closingIndex = findClosingHtmlTag(lowerSource, tagName.toLowerCase(), end);
190
236
  if (closingIndex === -1) {
237
+ if (htmlAutoClose && htmlAllowTags.has(tagNameLower)) {
238
+ const tail = source.slice(end);
239
+ const newlineCount = countNewlines(tail, htmlMaxNewlines + 1);
240
+ if (newlineCount <= htmlMaxNewlines) {
241
+ if (start > cursor) {
242
+ const absoluteFrom = baseIsFinite ? baseOffset + cursor : void 0;
243
+ const absoluteTo = baseIsFinite ? baseOffset + start : void 0;
244
+ pushTextSegment(segments, source.slice(cursor, start), absoluteFrom, absoluteTo, parseInline);
245
+ }
246
+ const rawSegment2 = source.slice(start);
247
+ const closedValue = `${rawSegment2}</${tagName}>`;
248
+ const segment2 = {
249
+ kind: "html",
250
+ value: closedValue,
251
+ range: createSegmentRange(baseOffset, start, source.length),
252
+ sanitized: sanitizeHtmlInWorker(closedValue)
253
+ };
254
+ segments.push(segment2);
255
+ cursor = source.length;
256
+ break;
257
+ }
258
+ }
191
259
  tagPattern.lastIndex = start + 1;
192
260
  match = tagPattern.exec(source);
193
261
  continue;
@@ -199,8 +267,8 @@ function splitByTagSegments(source, baseOffset, parseInline) {
199
267
  const absoluteTo = baseIsFinite ? baseOffset + start : void 0;
200
268
  pushTextSegment(segments, source.slice(cursor, start), absoluteFrom, absoluteTo, parseInline);
201
269
  }
202
- const rawSegment = source.slice(start, end);
203
- const kind = isLikelyMdxComponent(tagName) ? "mdx" : "html";
270
+ let rawSegment = source.slice(start, end);
271
+ const kind = mdxAllowed ? "mdx" : "html";
204
272
  const segment = {
205
273
  kind,
206
274
  value: rawSegment,
@@ -209,6 +277,17 @@ function splitByTagSegments(source, baseOffset, parseInline) {
209
277
  if (kind === "html") {
210
278
  segment.sanitized = sanitizeHtmlInWorker(rawSegment);
211
279
  } else {
280
+ const tail = source.slice(end);
281
+ const newlineCount = countNewlines(tail, mdxMaxNewlines + 1);
282
+ if (mdxAutoClose && newlineCount > mdxMaxNewlines) {
283
+ tagPattern.lastIndex = start + 1;
284
+ match = tagPattern.exec(source);
285
+ continue;
286
+ }
287
+ if (mdxAutoClose && !rawSegment.endsWith("/>")) {
288
+ rawSegment = selfCloseTag(rawSegment);
289
+ segment.value = rawSegment;
290
+ }
212
291
  segment.status = "pending";
213
292
  }
214
293
  segments.push(segment);
@@ -333,6 +412,48 @@ var VOID_HTML_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "meta", "input"
333
412
  function isVoidHtmlTag(tagName) {
334
413
  return VOID_HTML_TAGS.has(tagName.toLowerCase());
335
414
  }
415
+ function normalizeNewlineLimit(value) {
416
+ if (!Number.isFinite(value ?? Number.NaN)) {
417
+ return 2;
418
+ }
419
+ return Math.max(0, value ?? 0);
420
+ }
421
+ function normalizeHtmlAllowlist(value) {
422
+ if (!value) return DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS;
423
+ const tags = /* @__PURE__ */ new Set();
424
+ for (const tag of value) {
425
+ if (tag) {
426
+ tags.add(tag.toLowerCase());
427
+ }
428
+ }
429
+ return tags.size > 0 ? tags : DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS;
430
+ }
431
+ function normalizeComponentAllowlist(value) {
432
+ if (!value) return null;
433
+ const tags = /* @__PURE__ */ new Set();
434
+ for (const tag of value) {
435
+ if (tag) tags.add(tag);
436
+ }
437
+ return tags.size > 0 ? tags : null;
438
+ }
439
+ function countNewlines(value, limit) {
440
+ let count = 0;
441
+ for (let i = 0; i < value.length; i++) {
442
+ if (value.charCodeAt(i) === 10) {
443
+ count += 1;
444
+ if (limit !== void 0 && count >= limit) {
445
+ return count;
446
+ }
447
+ }
448
+ }
449
+ return count;
450
+ }
451
+ function selfCloseTag(rawTag) {
452
+ if (rawTag.endsWith("/>")) return rawTag;
453
+ const closeIndex = rawTag.lastIndexOf(">");
454
+ if (closeIndex === -1) return rawTag;
455
+ return `${rawTag.slice(0, closeIndex)}/>`;
456
+ }
336
457
  function isLikelyMdxComponent(tagName) {
337
458
  const first = tagName.charAt(0);
338
459
  return first.toUpperCase() === first && first.toLowerCase() !== first;
@@ -1,7 +1,21 @@
1
1
  import { InlineNode, MixedContentSegment } from './types.cjs';
2
2
 
3
- declare function extractMixedContentSegments(raw: string, baseOffset: number | undefined, parseInline: (content: string) => InlineNode[]): MixedContentSegment[];
3
+ interface MixedContentAutoCloseHtmlOptions {
4
+ autoClose?: boolean;
5
+ maxNewlines?: number;
6
+ allowTags?: Iterable<string>;
7
+ }
8
+ interface MixedContentAutoCloseMdxOptions {
9
+ autoClose?: boolean;
10
+ maxNewlines?: number;
11
+ componentAllowlist?: Iterable<string>;
12
+ }
13
+ interface MixedContentOptions {
14
+ html?: MixedContentAutoCloseHtmlOptions;
15
+ mdx?: MixedContentAutoCloseMdxOptions;
16
+ }
17
+ declare function extractMixedContentSegments(raw: string, baseOffset: number | undefined, parseInline: (content: string) => InlineNode[], options?: MixedContentOptions): MixedContentSegment[];
4
18
  declare function isLikelyMdxComponent(tagName: string): boolean;
5
19
  declare function findClosingHtmlTag(lowerSource: string, lowerTagName: string, startIndex: number): number;
6
20
 
7
- export { extractMixedContentSegments, findClosingHtmlTag, isLikelyMdxComponent };
21
+ export { type MixedContentAutoCloseHtmlOptions, type MixedContentAutoCloseMdxOptions, type MixedContentOptions, extractMixedContentSegments, findClosingHtmlTag, isLikelyMdxComponent };
@@ -1,7 +1,21 @@
1
1
  import { InlineNode, MixedContentSegment } from './types.js';
2
2
 
3
- declare function extractMixedContentSegments(raw: string, baseOffset: number | undefined, parseInline: (content: string) => InlineNode[]): MixedContentSegment[];
3
+ interface MixedContentAutoCloseHtmlOptions {
4
+ autoClose?: boolean;
5
+ maxNewlines?: number;
6
+ allowTags?: Iterable<string>;
7
+ }
8
+ interface MixedContentAutoCloseMdxOptions {
9
+ autoClose?: boolean;
10
+ maxNewlines?: number;
11
+ componentAllowlist?: Iterable<string>;
12
+ }
13
+ interface MixedContentOptions {
14
+ html?: MixedContentAutoCloseHtmlOptions;
15
+ mdx?: MixedContentAutoCloseMdxOptions;
16
+ }
17
+ declare function extractMixedContentSegments(raw: string, baseOffset: number | undefined, parseInline: (content: string) => InlineNode[], options?: MixedContentOptions): MixedContentSegment[];
4
18
  declare function isLikelyMdxComponent(tagName: string): boolean;
5
19
  declare function findClosingHtmlTag(lowerSource: string, lowerTagName: string, startIndex: number): number;
6
20
 
7
- export { extractMixedContentSegments, findClosingHtmlTag, isLikelyMdxComponent };
21
+ export { type MixedContentAutoCloseHtmlOptions, type MixedContentAutoCloseMdxOptions, type MixedContentOptions, extractMixedContentSegments, findClosingHtmlTag, isLikelyMdxComponent };
@@ -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 {
@@ -121,9 +135,27 @@ function mergeAttributes(existing, additions) {
121
135
  }
122
136
 
123
137
  // src/mixed-content.ts
124
- function extractMixedContentSegments(raw, baseOffset, parseInline) {
138
+ var DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS = /* @__PURE__ */ new Set([
139
+ "span",
140
+ "em",
141
+ "strong",
142
+ "code",
143
+ "kbd",
144
+ "del",
145
+ "s",
146
+ "mark",
147
+ "sub",
148
+ "sup",
149
+ "i",
150
+ "b",
151
+ "u",
152
+ "small",
153
+ "abbr",
154
+ "a"
155
+ ]);
156
+ function extractMixedContentSegments(raw, baseOffset, parseInline, options) {
125
157
  if (!raw) return [];
126
- const initial = splitByTagSegments(raw, baseOffset, parseInline);
158
+ const initial = splitByTagSegments(raw, baseOffset, parseInline, options);
127
159
  const expanded = [];
128
160
  for (const segment of initial) {
129
161
  if (segment.kind === "text") {
@@ -134,22 +166,58 @@ function extractMixedContentSegments(raw, baseOffset, parseInline) {
134
166
  }
135
167
  return mergeAdjacentTextSegments(expanded, parseInline);
136
168
  }
137
- function splitByTagSegments(source, baseOffset, parseInline) {
169
+ function splitByTagSegments(source, baseOffset, parseInline, options) {
138
170
  const segments = [];
139
171
  const lowerSource = source.toLowerCase();
140
172
  const tagPattern = /<([A-Za-z][\w:-]*)([^<>]*?)\/?>/g;
141
173
  let cursor = 0;
142
174
  let match = tagPattern.exec(source);
143
175
  const baseIsFinite = typeof baseOffset === "number" && Number.isFinite(baseOffset);
176
+ const htmlAllowTags = normalizeHtmlAllowlist(options?.html?.allowTags);
177
+ const htmlAutoClose = options?.html?.autoClose === true;
178
+ const htmlMaxNewlines = normalizeNewlineLimit(options?.html?.maxNewlines);
179
+ const mdxAutoClose = options?.mdx?.autoClose === true;
180
+ const mdxMaxNewlines = normalizeNewlineLimit(options?.mdx?.maxNewlines);
181
+ const mdxAllowlist = normalizeComponentAllowlist(options?.mdx?.componentAllowlist);
144
182
  while (match !== null) {
145
183
  const start = match.index;
146
184
  const tagName = match[1];
147
185
  const matchText = match[0];
148
- const isSelfClosing = matchText.endsWith("/>") || isVoidHtmlTag(tagName);
186
+ const tagNameLower = tagName.toLowerCase();
187
+ const isSelfClosing = matchText.endsWith("/>") || isVoidHtmlTag(tagNameLower);
188
+ const mdxCandidate = isLikelyMdxComponent(tagName);
189
+ const mdxAllowed = mdxCandidate && (!mdxAllowlist || mdxAllowlist.has(tagName));
190
+ if (mdxCandidate && mdxAllowlist && !mdxAllowed) {
191
+ tagPattern.lastIndex = start + 1;
192
+ match = tagPattern.exec(source);
193
+ continue;
194
+ }
149
195
  let end = tagPattern.lastIndex;
150
- if (!isSelfClosing && !isLikelyMdxComponent(tagName)) {
196
+ if (!isSelfClosing && !mdxAllowed) {
151
197
  const closingIndex = findClosingHtmlTag(lowerSource, tagName.toLowerCase(), end);
152
198
  if (closingIndex === -1) {
199
+ if (htmlAutoClose && htmlAllowTags.has(tagNameLower)) {
200
+ const tail = source.slice(end);
201
+ const newlineCount = countNewlines(tail, htmlMaxNewlines + 1);
202
+ if (newlineCount <= htmlMaxNewlines) {
203
+ if (start > cursor) {
204
+ const absoluteFrom = baseIsFinite ? baseOffset + cursor : void 0;
205
+ const absoluteTo = baseIsFinite ? baseOffset + start : void 0;
206
+ pushTextSegment(segments, source.slice(cursor, start), absoluteFrom, absoluteTo, parseInline);
207
+ }
208
+ const rawSegment2 = source.slice(start);
209
+ const closedValue = `${rawSegment2}</${tagName}>`;
210
+ const segment2 = {
211
+ kind: "html",
212
+ value: closedValue,
213
+ range: createSegmentRange(baseOffset, start, source.length),
214
+ sanitized: sanitizeHtmlInWorker(closedValue)
215
+ };
216
+ segments.push(segment2);
217
+ cursor = source.length;
218
+ break;
219
+ }
220
+ }
153
221
  tagPattern.lastIndex = start + 1;
154
222
  match = tagPattern.exec(source);
155
223
  continue;
@@ -161,8 +229,8 @@ function splitByTagSegments(source, baseOffset, parseInline) {
161
229
  const absoluteTo = baseIsFinite ? baseOffset + start : void 0;
162
230
  pushTextSegment(segments, source.slice(cursor, start), absoluteFrom, absoluteTo, parseInline);
163
231
  }
164
- const rawSegment = source.slice(start, end);
165
- const kind = isLikelyMdxComponent(tagName) ? "mdx" : "html";
232
+ let rawSegment = source.slice(start, end);
233
+ const kind = mdxAllowed ? "mdx" : "html";
166
234
  const segment = {
167
235
  kind,
168
236
  value: rawSegment,
@@ -171,6 +239,17 @@ function splitByTagSegments(source, baseOffset, parseInline) {
171
239
  if (kind === "html") {
172
240
  segment.sanitized = sanitizeHtmlInWorker(rawSegment);
173
241
  } else {
242
+ const tail = source.slice(end);
243
+ const newlineCount = countNewlines(tail, mdxMaxNewlines + 1);
244
+ if (mdxAutoClose && newlineCount > mdxMaxNewlines) {
245
+ tagPattern.lastIndex = start + 1;
246
+ match = tagPattern.exec(source);
247
+ continue;
248
+ }
249
+ if (mdxAutoClose && !rawSegment.endsWith("/>")) {
250
+ rawSegment = selfCloseTag(rawSegment);
251
+ segment.value = rawSegment;
252
+ }
174
253
  segment.status = "pending";
175
254
  }
176
255
  segments.push(segment);
@@ -295,6 +374,48 @@ var VOID_HTML_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "meta", "input"
295
374
  function isVoidHtmlTag(tagName) {
296
375
  return VOID_HTML_TAGS.has(tagName.toLowerCase());
297
376
  }
377
+ function normalizeNewlineLimit(value) {
378
+ if (!Number.isFinite(value ?? Number.NaN)) {
379
+ return 2;
380
+ }
381
+ return Math.max(0, value ?? 0);
382
+ }
383
+ function normalizeHtmlAllowlist(value) {
384
+ if (!value) return DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS;
385
+ const tags = /* @__PURE__ */ new Set();
386
+ for (const tag of value) {
387
+ if (tag) {
388
+ tags.add(tag.toLowerCase());
389
+ }
390
+ }
391
+ return tags.size > 0 ? tags : DEFAULT_INLINE_HTML_AUTOCLOSE_TAGS;
392
+ }
393
+ function normalizeComponentAllowlist(value) {
394
+ if (!value) return null;
395
+ const tags = /* @__PURE__ */ new Set();
396
+ for (const tag of value) {
397
+ if (tag) tags.add(tag);
398
+ }
399
+ return tags.size > 0 ? tags : null;
400
+ }
401
+ function countNewlines(value, limit) {
402
+ let count = 0;
403
+ for (let i = 0; i < value.length; i++) {
404
+ if (value.charCodeAt(i) === 10) {
405
+ count += 1;
406
+ if (limit !== void 0 && count >= limit) {
407
+ return count;
408
+ }
409
+ }
410
+ }
411
+ return count;
412
+ }
413
+ function selfCloseTag(rawTag) {
414
+ if (rawTag.endsWith("/>")) return rawTag;
415
+ const closeIndex = rawTag.lastIndexOf(">");
416
+ if (closeIndex === -1) return rawTag;
417
+ return `${rawTag.slice(0, closeIndex)}/>`;
418
+ }
298
419
  function isLikelyMdxComponent(tagName) {
299
420
  const first = tagName.charAt(0);
300
421
  return first.toUpperCase() === first && first.toLowerCase() !== first;
@@ -20,69 +20,134 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/streaming/inline-streaming.ts
21
21
  var inline_streaming_exports = {};
22
22
  __export(inline_streaming_exports, {
23
+ normalizeFormatAnticipation: () => normalizeFormatAnticipation,
23
24
  prepareInlineStreamingContent: () => prepareInlineStreamingContent
24
25
  });
25
26
  module.exports = __toCommonJS(inline_streaming_exports);
27
+ var DEFAULT_FORMAT_ANTICIPATION = {
28
+ inline: false,
29
+ mathInline: false,
30
+ mathBlock: false,
31
+ html: false,
32
+ mdx: false,
33
+ regex: false
34
+ };
35
+ function normalizeFormatAnticipation(input) {
36
+ if (input === true) {
37
+ return { ...DEFAULT_FORMAT_ANTICIPATION, inline: true };
38
+ }
39
+ if (!input) {
40
+ return { ...DEFAULT_FORMAT_ANTICIPATION };
41
+ }
42
+ return {
43
+ inline: input.inline ?? false,
44
+ mathInline: input.mathInline ?? false,
45
+ mathBlock: input.mathBlock ?? false,
46
+ html: input.html ?? false,
47
+ mdx: input.mdx ?? false,
48
+ regex: input.regex ?? false
49
+ };
50
+ }
26
51
  function prepareInlineStreamingContent(content, options) {
27
- const enableAnticipation = Boolean(options?.formatAnticipation);
28
52
  const enableMath = options?.math !== false;
29
- let dollarCount = 0;
30
- let backtickCount = 0;
31
- let starCount = 0;
32
- let doubleStarCount = 0;
33
- let tildePairCount = 0;
53
+ const anticipation = normalizeFormatAnticipation(options?.formatAnticipation);
54
+ const enableInlineAnticipation = anticipation.inline;
55
+ const enableMathInlineAnticipation = anticipation.mathInline;
56
+ const enableMathBlockAnticipation = anticipation.mathBlock;
57
+ const stack = [];
58
+ const toggleToken = (token) => {
59
+ const last = stack[stack.length - 1];
60
+ if (last === token) {
61
+ stack.pop();
62
+ } else {
63
+ stack.push(token);
64
+ }
65
+ };
66
+ let mathDisplayOpen = false;
67
+ let mathDisplayCrossedNewline = false;
34
68
  for (let i = 0; i < content.length; i++) {
35
69
  const code = content.charCodeAt(i);
36
- if (code === 36) {
37
- dollarCount += 1;
70
+ if (code === 10 || code === 13) {
71
+ if (mathDisplayOpen) {
72
+ mathDisplayCrossedNewline = true;
73
+ }
38
74
  continue;
39
75
  }
40
76
  if (code === 96) {
41
- backtickCount += 1;
77
+ toggleToken("code");
78
+ continue;
79
+ }
80
+ if (code === 126 && i + 1 < content.length && content.charCodeAt(i + 1) === 126) {
81
+ toggleToken("strike");
82
+ i += 1;
42
83
  continue;
43
84
  }
44
85
  if (code === 42) {
45
86
  if (i + 1 < content.length && content.charCodeAt(i + 1) === 42) {
46
- doubleStarCount += 1;
47
- starCount += 2;
87
+ toggleToken("strong");
48
88
  i += 1;
49
89
  } else {
50
- starCount += 1;
90
+ toggleToken("em");
51
91
  }
52
92
  continue;
53
93
  }
54
- if (code === 126) {
55
- if (i + 1 < content.length && content.charCodeAt(i + 1) === 126) {
56
- tildePairCount += 1;
94
+ if (enableMath && code === 36) {
95
+ if (i + 1 < content.length && content.charCodeAt(i + 1) === 36) {
96
+ toggleToken("math-display");
97
+ if (mathDisplayOpen) {
98
+ mathDisplayOpen = false;
99
+ mathDisplayCrossedNewline = false;
100
+ } else {
101
+ mathDisplayOpen = true;
102
+ mathDisplayCrossedNewline = false;
103
+ }
57
104
  i += 1;
105
+ } else {
106
+ toggleToken("math-inline");
58
107
  }
59
108
  }
60
109
  }
61
- const hasIncompleteMath = enableMath && dollarCount % 2 !== 0;
62
- if (hasIncompleteMath) {
63
- return { kind: "raw", status: "raw", reason: "incomplete-math" };
64
- }
65
- const hasIncompleteCode = backtickCount % 2 !== 0;
66
- const hasIncompleteStrong = doubleStarCount % 2 !== 0;
67
- const singleStarCount = starCount - doubleStarCount * 2;
68
- const hasIncompleteEmphasis = singleStarCount % 2 !== 0;
69
- const hasIncompleteStrike = tildePairCount % 2 !== 0;
70
- const hasAnyIncomplete = hasIncompleteCode || hasIncompleteStrong || hasIncompleteEmphasis || hasIncompleteStrike;
71
- if (!hasAnyIncomplete) {
72
- return { kind: "parse", status: "complete", content, appended: "" };
110
+ const hasIncompleteFormatting = stack.some((token) => token === "code" || token === "strike" || token === "strong" || token === "em");
111
+ const hasIncompleteMathInline = stack.includes("math-inline");
112
+ const hasIncompleteMathDisplay = stack.includes("math-display");
113
+ const hasIncompleteMath = hasIncompleteMathInline || hasIncompleteMathDisplay;
114
+ if (enableMath && hasIncompleteMath) {
115
+ if (hasIncompleteMathInline && !enableMathInlineAnticipation) {
116
+ return { kind: "raw", status: "raw", reason: "incomplete-math" };
117
+ }
118
+ if (hasIncompleteMathDisplay && (!enableMathBlockAnticipation || mathDisplayCrossedNewline)) {
119
+ return { kind: "raw", status: "raw", reason: "incomplete-math" };
120
+ }
73
121
  }
74
- if (!enableAnticipation) {
122
+ if (hasIncompleteFormatting && !enableInlineAnticipation) {
75
123
  return { kind: "raw", status: "raw", reason: "incomplete-formatting" };
76
124
  }
77
- let appended = "";
78
- if (hasIncompleteCode) appended += "`";
79
- if (hasIncompleteStrike) appended += "~~";
80
- if (hasIncompleteStrong && hasIncompleteEmphasis) appended += "***";
81
- else if (hasIncompleteStrong) appended += "**";
82
- else if (hasIncompleteEmphasis) appended += "*";
125
+ if (!hasIncompleteFormatting && !hasIncompleteMath) {
126
+ return { kind: "parse", status: "complete", content, appended: "" };
127
+ }
128
+ const appendForToken = (token) => {
129
+ switch (token) {
130
+ case "code":
131
+ return "`";
132
+ case "strike":
133
+ return "~~";
134
+ case "strong":
135
+ return "**";
136
+ case "em":
137
+ return "*";
138
+ case "math-inline":
139
+ return "$";
140
+ case "math-display":
141
+ return "$$";
142
+ default:
143
+ return "";
144
+ }
145
+ };
146
+ const appended = stack.slice().reverse().map((token) => appendForToken(token)).join("");
83
147
  return { kind: "parse", status: "anticipated", content: content + appended, appended };
84
148
  }
85
149
  // Annotate the CommonJS export names for ESM import in node:
86
150
  0 && (module.exports = {
151
+ normalizeFormatAnticipation,
87
152
  prepareInlineStreamingContent
88
153
  });
@@ -1,4 +1,15 @@
1
+ import { FormatAnticipationConfig } from '../types.cjs';
2
+
1
3
  type InlineStreamingInlineStatus = "complete" | "anticipated" | "raw";
4
+ type NormalizedFormatAnticipation = {
5
+ inline: boolean;
6
+ mathInline: boolean;
7
+ mathBlock: boolean;
8
+ html: boolean;
9
+ mdx: boolean;
10
+ regex: boolean;
11
+ };
12
+ declare function normalizeFormatAnticipation(input?: FormatAnticipationConfig): NormalizedFormatAnticipation;
2
13
  type InlineStreamingPrepareResult = {
3
14
  kind: "raw";
4
15
  status: "raw";
@@ -10,8 +21,8 @@ type InlineStreamingPrepareResult = {
10
21
  appended: string;
11
22
  };
12
23
  declare function prepareInlineStreamingContent(content: string, options?: {
13
- formatAnticipation?: boolean;
24
+ formatAnticipation?: FormatAnticipationConfig;
14
25
  math?: boolean;
15
26
  }): InlineStreamingPrepareResult;
16
27
 
17
- export { type InlineStreamingInlineStatus, type InlineStreamingPrepareResult, prepareInlineStreamingContent };
28
+ export { type InlineStreamingInlineStatus, type InlineStreamingPrepareResult, type NormalizedFormatAnticipation, normalizeFormatAnticipation, prepareInlineStreamingContent };
@@ -1,4 +1,15 @@
1
+ import { FormatAnticipationConfig } from '../types.js';
2
+
1
3
  type InlineStreamingInlineStatus = "complete" | "anticipated" | "raw";
4
+ type NormalizedFormatAnticipation = {
5
+ inline: boolean;
6
+ mathInline: boolean;
7
+ mathBlock: boolean;
8
+ html: boolean;
9
+ mdx: boolean;
10
+ regex: boolean;
11
+ };
12
+ declare function normalizeFormatAnticipation(input?: FormatAnticipationConfig): NormalizedFormatAnticipation;
2
13
  type InlineStreamingPrepareResult = {
3
14
  kind: "raw";
4
15
  status: "raw";
@@ -10,8 +21,8 @@ type InlineStreamingPrepareResult = {
10
21
  appended: string;
11
22
  };
12
23
  declare function prepareInlineStreamingContent(content: string, options?: {
13
- formatAnticipation?: boolean;
24
+ formatAnticipation?: FormatAnticipationConfig;
14
25
  math?: boolean;
15
26
  }): InlineStreamingPrepareResult;
16
27
 
17
- export { type InlineStreamingInlineStatus, type InlineStreamingPrepareResult, prepareInlineStreamingContent };
28
+ export { type InlineStreamingInlineStatus, type InlineStreamingPrepareResult, type NormalizedFormatAnticipation, normalizeFormatAnticipation, prepareInlineStreamingContent };