@antdv-next/x-markdown 0.1.4 → 0.3.0

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.
Files changed (28) hide show
  1. package/dist/XMarkdown/components/AnimationText.vue.d.ts +6 -0
  2. package/dist/XMarkdown/components/AnimationText.vue_vue_type_script_setup_true_lang.js +58 -2
  3. package/dist/XMarkdown/components/Section.js +5 -0
  4. package/dist/XMarkdown/components/Section.vue.d.ts +76 -0
  5. package/dist/XMarkdown/components/Section.vue_vue_type_script_lang.js +54 -0
  6. package/dist/XMarkdown/composables/index.d.ts +2 -1
  7. package/dist/XMarkdown/composables/index.js +3 -2
  8. package/dist/XMarkdown/composables/useStreaming.d.ts +37 -1
  9. package/dist/XMarkdown/composables/useStreaming.js +449 -34
  10. package/dist/XMarkdown/composables/useTypewriter.d.ts +11 -0
  11. package/dist/XMarkdown/composables/useTypewriter.js +378 -0
  12. package/dist/XMarkdown/core/Parser.d.ts +15 -0
  13. package/dist/XMarkdown/core/Parser.js +42 -14
  14. package/dist/XMarkdown/core/VueRenderer.js +16 -9
  15. package/dist/XMarkdown/core/detectUnclosedComponentTags.d.ts +12 -0
  16. package/dist/XMarkdown/core/detectUnclosedComponentTags.js +240 -6
  17. package/dist/XMarkdown/core/index.js +1 -1
  18. package/dist/XMarkdown/index.d.ts +6 -2
  19. package/dist/XMarkdown/index.js +5 -3
  20. package/dist/XMarkdown/index.vue.d.ts +1 -1
  21. package/dist/XMarkdown/index.vue_vue_type_script_setup_true_lang.js +64 -30
  22. package/dist/XMarkdown/interface.d.ts +241 -14
  23. package/dist/XMarkdown/utils/streaming.d.ts +6 -0
  24. package/dist/XMarkdown/utils/streaming.js +32 -0
  25. package/dist/index.d.ts +6 -2
  26. package/dist/index.js +5 -3
  27. package/package.json +2 -2
  28. package/plugins/Latex/index.js +8 -2
@@ -0,0 +1,378 @@
1
+ import { computed, onScopeDispose, ref, watch } from "vue";
2
+ //#region src/XMarkdown/composables/useTypewriter.ts
3
+ /**
4
+ * Paces a growing string so it is revealed character by character (or
5
+ * sentence by sentence) at a rate that follows the incoming chunks: content
6
+ * that arrives in bursts is smoothed out instead of appearing as blocks.
7
+ *
8
+ * The output is always a prefix of the input, so downstream prefix caches
9
+ * (useStreaming) keep working. When `active` is false the input is passed
10
+ * through untouched, which also makes the composable a no-op outside the
11
+ * browser.
12
+ */
13
+ var DEFAULT_MIN_CPS = 24;
14
+ var DEFAULT_MAX_CPS = 3e3;
15
+ var DEFAULT_DELIMITERS = [
16
+ "。",
17
+ "!",
18
+ "?",
19
+ ".",
20
+ "!",
21
+ "?",
22
+ "\n"
23
+ ];
24
+ var DEFAULT_MAX_SENTENCE_CHARS = 120;
25
+ var DEFAULT_CHUNK_INTERVAL_MS = 140;
26
+ var MIN_HORIZON_MS = 80;
27
+ var MAX_HORIZON_MS = 260;
28
+ var EWMA_ALPHA = .28;
29
+ var clamp = (value, min, max) => value < min ? min : value > max ? max : value;
30
+ var isHighSurrogate = (text, index) => {
31
+ const code = text.charCodeAt(index);
32
+ return code >= 55296 && code <= 56319;
33
+ };
34
+ /** Never cut between the two halves of a surrogate pair. */
35
+ var alignToCodePoint = (text, length) => length > 0 && length < text.length && isHighSurrogate(text, length - 1) ? length + 1 : length;
36
+ var ZWJ = 8205;
37
+ var isVariationSelector = (cp) => cp === 65038 || cp === 65039;
38
+ var isSkinTone = (cp) => cp >= 127995 && cp <= 127999;
39
+ var isCombiningMark = (cp) => cp >= 768 && cp <= 879 || cp >= 6832 && cp <= 6911 || cp >= 8400 && cp <= 8447;
40
+ var isRegionalIndicator = (cp) => cp >= 127462 && cp <= 127487;
41
+ var isTagCharacter = (cp) => cp >= 917536 && cp <= 917631;
42
+ var KEYCAP = 8419;
43
+ /** Code point starting at `index`, and the index right after it. */
44
+ var codePointAt = (text, index) => {
45
+ const cp = text.codePointAt(index);
46
+ return [cp, index + (cp > 65535 ? 2 : 1)];
47
+ };
48
+ var codePointBefore = (text, index) => {
49
+ const start = index >= 2 && isHighSurrogate(text, index - 2) ? index - 2 : index - 1;
50
+ return [text.codePointAt(start), start];
51
+ };
52
+ var alignToClusterFallback = (text, length) => {
53
+ let end = alignToCodePoint(text, length);
54
+ let indicators = 0;
55
+ for (let i = end; i > 0;) {
56
+ const [cp, start] = codePointBefore(text, i);
57
+ if (!isRegionalIndicator(cp)) break;
58
+ indicators += 1;
59
+ i = start;
60
+ }
61
+ for (;;) {
62
+ if (end >= text.length) return text.length;
63
+ const [prev] = codePointBefore(text, end);
64
+ const [next, after] = codePointAt(text, end);
65
+ if (next === ZWJ || prev === ZWJ || isVariationSelector(next) || isSkinTone(next) || isCombiningMark(next) || next === KEYCAP || isTagCharacter(next)) {
66
+ end = after;
67
+ continue;
68
+ }
69
+ if (isRegionalIndicator(next) && isRegionalIndicator(prev) && indicators % 2 === 1) {
70
+ end = after;
71
+ indicators += 1;
72
+ continue;
73
+ }
74
+ return end;
75
+ }
76
+ };
77
+ var segmenter;
78
+ var getSegmenter = () => {
79
+ if (segmenter === void 0) {
80
+ const Segmenter = typeof Intl !== "undefined" ? Intl.Segmenter : void 0;
81
+ segmenter = typeof Segmenter === "function" ? new Segmenter(void 0, { granularity: "grapheme" }) : null;
82
+ }
83
+ return segmenter;
84
+ };
85
+ var CLUSTER_WINDOW = 32;
86
+ var CLUSTER_CONTINUER = /^(?:\p{Grapheme_Extend}|\p{Regional_Indicator}|‍)$/u;
87
+ /** Index of the code point that ends at `pos`. */
88
+ var prevCodePointStart = (text, pos) => {
89
+ const code = text.charCodeAt(pos - 1);
90
+ return code >= 56320 && code <= 57343 && pos > 1 ? pos - 2 : pos - 1;
91
+ };
92
+ /**
93
+ * Move the window start back to a position the segmenter can treat as a real
94
+ * cluster start. Starting the window mid-cluster (e.g. between the two
95
+ * regional indicators of a flag, or before a combining mark) would make the
96
+ * segmenter report a boundary that does not exist in the full string.
97
+ */
98
+ var safeWindowStart = (text, from) => {
99
+ let pos = Math.max(0, alignToCodePoint(text, from));
100
+ while (pos > 0) {
101
+ const prevStart = prevCodePointStart(text, pos);
102
+ const prev = text.codePointAt(prevStart);
103
+ const current = text.codePointAt(pos);
104
+ if (prev !== ZWJ && !CLUSTER_CONTINUER.test(String.fromCodePoint(current))) break;
105
+ pos = prevStart;
106
+ }
107
+ return pos;
108
+ };
109
+ /**
110
+ * Never cut inside a user-perceived character: a cut is moved forward to the
111
+ * next grapheme-cluster boundary so an emoji, a flag or an accented letter
112
+ * is never shown half-built (or as a replacement glyph) for a frame.
113
+ */
114
+ var alignToGrapheme = (text, length) => {
115
+ if (length <= 0 || length >= text.length) return length;
116
+ const seg = getSegmenter();
117
+ if (!seg) return alignToClusterFallback(text, length);
118
+ const start = safeWindowStart(text, length - CLUSTER_WINDOW);
119
+ const windowEnd = Math.min(text.length, alignToCodePoint(text, length + CLUSTER_WINDOW));
120
+ for (const { index, segment } of seg.segment(text.slice(start, windowEnd))) {
121
+ const clusterStart = start + index;
122
+ const clusterEnd = clusterStart + segment.length;
123
+ if (clusterStart >= length) return clusterStart;
124
+ if (clusterEnd >= length) return clusterEnd;
125
+ }
126
+ return windowEnd;
127
+ };
128
+ /**
129
+ * The furthest position that may be revealed while the stream is still going:
130
+ * a trailing high surrogate would render as U+FFFD until its other half
131
+ * arrives, and a trailing ZWJ leaves a half-joined cluster on screen, so both
132
+ * are held back by one code unit. At stream end `enabled` is false and the
133
+ * full text is shown as-is.
134
+ */
135
+ var streamSafeEnd = (text) => {
136
+ const length = text.length;
137
+ if (length === 0) return 0;
138
+ const last = text.charCodeAt(length - 1);
139
+ if (isHighSurrogate(text, length - 1) || last === ZWJ) return length - 1;
140
+ return length;
141
+ };
142
+ var initialScan = () => ({
143
+ scanned: 0,
144
+ boundaries: [],
145
+ inFence: false,
146
+ fenceChar: "",
147
+ fenceLen: 0,
148
+ atLineStart: true,
149
+ lineIndent: 0,
150
+ lineFenceChar: "",
151
+ lineFenceLen: 0,
152
+ lineTailBlank: true,
153
+ inInlineCode: false,
154
+ inlineCodeLen: 0,
155
+ backtickRun: 0
156
+ });
157
+ /**
158
+ * Settle the run of backticks that just ended. Outside code a run opens a
159
+ * span that only a run of the same length closes again: a span opened with
160
+ * two backticks is not ended by a single backtick inside it.
161
+ */
162
+ var settleBacktickRun = (scan) => {
163
+ const run = scan.backtickRun;
164
+ scan.backtickRun = 0;
165
+ if (run === 0) return;
166
+ if (scan.inInlineCode) {
167
+ if (run === scan.inlineCodeLen) {
168
+ scan.inInlineCode = false;
169
+ scan.inlineCodeLen = 0;
170
+ }
171
+ } else {
172
+ scan.inInlineCode = true;
173
+ scan.inlineCodeLen = run;
174
+ }
175
+ };
176
+ /**
177
+ * Extend the sentence-boundary scan to cover `text`. Delimiters inside fenced
178
+ * or inline code are not boundaries (`a.b.c`, `foo();`), newlines always are.
179
+ * Incremental: only the characters appended since the last call are visited.
180
+ */
181
+ var scanBoundaries = (scan, text, delimiters) => {
182
+ for (let i = scan.scanned; i < text.length; i++) {
183
+ const char = text[i];
184
+ if (char === "\n") {
185
+ if (scan.lineFenceLen >= 3) {
186
+ if (!scan.inFence) {
187
+ scan.inFence = true;
188
+ scan.fenceChar = scan.lineFenceChar;
189
+ scan.fenceLen = scan.lineFenceLen;
190
+ } else if (scan.lineFenceChar === scan.fenceChar && scan.lineFenceLen >= scan.fenceLen && scan.lineTailBlank) {
191
+ scan.inFence = false;
192
+ scan.fenceChar = "";
193
+ scan.fenceLen = 0;
194
+ }
195
+ }
196
+ scan.lineFenceChar = "";
197
+ scan.lineFenceLen = 0;
198
+ scan.lineIndent = 0;
199
+ scan.lineTailBlank = true;
200
+ scan.atLineStart = true;
201
+ scan.inInlineCode = false;
202
+ scan.inlineCodeLen = 0;
203
+ scan.backtickRun = 0;
204
+ scan.boundaries.push(i + 1);
205
+ continue;
206
+ }
207
+ if (scan.atLineStart) {
208
+ if (char === " " && scan.lineFenceLen === 0 && scan.lineIndent < 3) {
209
+ scan.lineIndent += 1;
210
+ continue;
211
+ }
212
+ if ((char === "`" || char === "~") && (scan.lineFenceLen === 0 || scan.lineFenceChar === char)) {
213
+ scan.lineFenceChar = char;
214
+ scan.lineFenceLen += 1;
215
+ continue;
216
+ }
217
+ scan.atLineStart = false;
218
+ scan.lineTailBlank = /\s/.test(char);
219
+ if (scan.lineFenceChar === "`" && scan.lineFenceLen < 3) {
220
+ scan.inInlineCode = true;
221
+ scan.inlineCodeLen = scan.lineFenceLen;
222
+ }
223
+ } else scan.lineTailBlank = scan.lineTailBlank && /\s/.test(char);
224
+ if (scan.inFence) continue;
225
+ if (char === "`") {
226
+ scan.backtickRun += 1;
227
+ continue;
228
+ }
229
+ settleBacktickRun(scan);
230
+ if (!scan.inInlineCode && delimiters.has(char)) scan.boundaries.push(i + 1);
231
+ }
232
+ scan.scanned = text.length;
233
+ };
234
+ /** Largest boundary that is <= `length`, or 0. */
235
+ var floorToBoundary = (boundaries, length) => {
236
+ let lo = 0;
237
+ let hi = boundaries.length - 1;
238
+ let best = 0;
239
+ while (lo <= hi) {
240
+ const mid = lo + hi >> 1;
241
+ if (boundaries[mid] <= length) {
242
+ best = boundaries[mid];
243
+ lo = mid + 1;
244
+ } else hi = mid - 1;
245
+ }
246
+ return best;
247
+ };
248
+ var resolveConfig = (typewriter) => {
249
+ const config = typeof typewriter === "object" ? typewriter : {};
250
+ return {
251
+ unit: config.unit ?? "char",
252
+ delimiters: config.delimiters ?? DEFAULT_DELIMITERS,
253
+ minCps: config.minCps ?? DEFAULT_MIN_CPS,
254
+ maxCps: config.maxCps ?? DEFAULT_MAX_CPS,
255
+ pauseMs: config.pauseMs ?? 0,
256
+ maxSentenceChars: config.maxSentenceChars ?? DEFAULT_MAX_SENTENCE_CHARS
257
+ };
258
+ };
259
+ var canAnimate = () => typeof window !== "undefined" && typeof window.requestAnimationFrame === "function";
260
+ function useTypewriter(input, typewriter, active) {
261
+ const enabled = computed(() => !!typewriter.value && active.value && canAnimate());
262
+ const config = computed(() => resolveConfig(typewriter.value));
263
+ const delimiterSet = computed(() => new Set(config.value.delimiters));
264
+ const displayLength = ref(input.value.length);
265
+ let cursor = input.value.length;
266
+ let lastChunkAt = null;
267
+ let chunkInterval = DEFAULT_CHUNK_INTERVAL_MS;
268
+ let raf = null;
269
+ let lastFrameAt = null;
270
+ let pauseUntil = 0;
271
+ let scan = initialScan();
272
+ let wasEnabled = enabled.value;
273
+ const stop = () => {
274
+ if (raf != null) {
275
+ window.cancelAnimationFrame(raf);
276
+ raf = null;
277
+ }
278
+ lastFrameAt = null;
279
+ };
280
+ const commit = (length) => {
281
+ if (length !== displayLength.value) displayLength.value = length;
282
+ };
283
+ const tick = (now) => {
284
+ raf = null;
285
+ const text = input.value;
286
+ const target = enabled.value ? streamSafeEnd(text) : text.length;
287
+ if (!enabled.value || displayLength.value >= target) {
288
+ stop();
289
+ return;
290
+ }
291
+ const deltaMs = Math.max(8, now - (lastFrameAt ?? now));
292
+ lastFrameAt = now;
293
+ const { unit, minCps, maxCps, pauseMs, maxSentenceChars } = config.value;
294
+ if (now >= pauseUntil) {
295
+ const cps = clamp((target - cursor) / clamp(chunkInterval, MIN_HORIZON_MS, MAX_HORIZON_MS) * 1e3, minCps, maxCps);
296
+ cursor = Math.min(target, cursor + cps * deltaMs / 1e3);
297
+ let next;
298
+ if (unit === "sentence") {
299
+ scanBoundaries(scan, text, delimiterSet.value);
300
+ const { boundaries } = scan;
301
+ const shown = displayLength.value;
302
+ const cursorFloor = Math.floor(cursor);
303
+ const boundary = floorToBoundary(boundaries, cursorFloor);
304
+ if (cursorFloor >= target) next = target;
305
+ else if (boundary > shown) next = boundary;
306
+ else if (cursorFloor - floorToBoundary(boundaries, shown) > maxSentenceChars) next = Math.min(target, alignToGrapheme(text, Math.max(shown + 1, cursorFloor)));
307
+ else next = shown;
308
+ } else {
309
+ next = Math.max(displayLength.value + 1, Math.floor(cursor));
310
+ next = Math.min(target, alignToGrapheme(text, next));
311
+ if (pauseMs > 0 && next > displayLength.value) {
312
+ scanBoundaries(scan, text, delimiterSet.value);
313
+ const boundary = floorToBoundary(scan.boundaries, next);
314
+ if (boundary > displayLength.value) {
315
+ next = boundary;
316
+ cursor = boundary;
317
+ pauseUntil = now + pauseMs;
318
+ }
319
+ }
320
+ }
321
+ commit(next);
322
+ }
323
+ if (displayLength.value < target) raf = window.requestAnimationFrame(tick);
324
+ else stop();
325
+ };
326
+ const schedule = () => {
327
+ if (raf != null || !enabled.value) return;
328
+ if (displayLength.value >= input.value.length) return;
329
+ lastFrameAt = null;
330
+ raf = window.requestAnimationFrame(tick);
331
+ };
332
+ watch([input, enabled], ([text, on], [previous]) => {
333
+ if (!on) {
334
+ stop();
335
+ wasEnabled = false;
336
+ return;
337
+ }
338
+ if (!wasEnabled) {
339
+ wasEnabled = true;
340
+ scan = initialScan();
341
+ cursor = text.length;
342
+ commit(text.length);
343
+ lastChunkAt = null;
344
+ chunkInterval = DEFAULT_CHUNK_INTERVAL_MS;
345
+ return;
346
+ }
347
+ if (!text.startsWith(previous.slice(0, displayLength.value))) {
348
+ stop();
349
+ scan = initialScan();
350
+ cursor = text.length;
351
+ commit(text.length);
352
+ lastChunkAt = null;
353
+ chunkInterval = DEFAULT_CHUNK_INTERVAL_MS;
354
+ return;
355
+ }
356
+ if (!text.startsWith(previous)) {
357
+ scan = initialScan();
358
+ cursor = Math.min(cursor, text.length);
359
+ }
360
+ if (text.length > previous.length) {
361
+ const now = performance.now();
362
+ if (lastChunkAt != null) {
363
+ const interval = clamp(now - lastChunkAt, 30, 500);
364
+ chunkInterval = clamp((1 - EWMA_ALPHA) * chunkInterval + EWMA_ALPHA * interval, MIN_HORIZON_MS, MAX_HORIZON_MS);
365
+ }
366
+ lastChunkAt = now;
367
+ }
368
+ schedule();
369
+ });
370
+ onScopeDispose(stop);
371
+ return computed(() => {
372
+ const text = input.value;
373
+ if (!enabled.value) return text;
374
+ return displayLength.value >= text.length ? text : text.slice(0, displayLength.value);
375
+ });
376
+ }
377
+ //#endregion
378
+ export { alignToClusterFallback, alignToGrapheme, useTypewriter as default, useTypewriter };
@@ -1,3 +1,4 @@
1
+ import { Token } from 'marked';
1
2
  import { ParserOptions } from '../interface';
2
3
  export declare function escapeHtml(html: string): string;
3
4
  export declare class Parser {
@@ -13,9 +14,23 @@ export declare class Parser {
13
14
  private prepareCodeBlockStates;
14
15
  private consumeCodeBlockState;
15
16
  private getCodeBlockState;
17
+ /**
18
+ * Block-lex `markdown` with the same marked configuration this parser
19
+ * renders with **and the same preprocessing** — the streaming section
20
+ * splitter uses it to decide whether a `#` line really starts a new
21
+ * top-level block, so the splitter and the renderer can never disagree.
22
+ * Lexing the raw source instead would ask the two about different documents:
23
+ * `protectCustomTags` deletes whole regions the renderer never sees, and
24
+ * `escapeRawHtml` turns tags into text, either of which moves a heading or a
25
+ * definition across a boundary. `config.extensions` (the bundled LaTeX
26
+ * plugin's `$$` rule) come along through `markdownInstance`.
27
+ */
28
+ lex(markdown: string): readonly Token[];
16
29
  parse(markdown: string, parseOptions?: {
17
30
  injectTail?: boolean;
18
31
  }): string;
32
+ /** Everything `parse` does to the source before marked sees it. */
33
+ private preprocess;
19
34
  private markedParse;
20
35
  private protectCustomTags;
21
36
  private escapeRawHtml;
@@ -45,21 +45,30 @@ var Parser = class Parser {
45
45
  this.configureRenderers();
46
46
  }
47
47
  configureRenderers() {
48
+ const options = this.options;
49
+ const consumeCodeBlockState = (lang) => this.consumeCodeBlockState(lang);
50
+ const markedParseInline = (renderer, tokens) => {
51
+ const parser = renderer.parser;
52
+ return parser ? parser.parseInline(tokens ?? []) : "";
53
+ };
48
54
  this.markdownInstance.use({ renderer: {
49
- html: (html) => {
50
- if (this.options.escapeRawHtml) return escapeHtml(html);
51
- return html;
55
+ html(token) {
56
+ if (options.escapeRawHtml) return escapeHtml(token.text);
57
+ return token.text;
52
58
  },
53
- link: (href, title, text) => {
54
- return `<a href="${href}"${title ? ` title="${title}"` : ""}${this.options.openLinksInNewTab ? ` target="_blank" rel="noopener noreferrer"` : ""}>${text}</a>`;
59
+ link(token) {
60
+ const titleAttr = token.title ? ` title="${token.title}"` : "";
61
+ const targetAttr = options.openLinksInNewTab ? ` target="_blank" rel="noopener noreferrer"` : "";
62
+ return `<a href="${token.href}"${titleAttr}${targetAttr}>${markedParseInline(this, token.tokens)}</a>`;
55
63
  },
56
- paragraph: (text) => {
57
- if (this.options.paragraphTag === "p") return `<p>${text}</p>`;
58
- return `<${this.options.paragraphTag}>${text}</${this.options.paragraphTag}>`;
64
+ paragraph(token) {
65
+ const text = markedParseInline(this, token.tokens);
66
+ if (options.paragraphTag === "p") return `<p>${text}</p>`;
67
+ return `<${options.paragraphTag}>${text}</${options.paragraphTag}>`;
59
68
  },
60
- code: (code, infostring, escaped) => {
61
- const lang = infostring || "";
62
- return `<pre><code class="language-${lang}"${lang ? ` data-lang="${lang}"` : ""} data-block="true"${` data-state="${this.consumeCodeBlockState(lang)}"`}>${escaped ? code : escapeHtml(code)}</code></pre>`;
69
+ code(token) {
70
+ const lang = token.lang || "";
71
+ return `<pre><code class="language-${lang}"${lang ? ` data-lang="${lang}"` : ""} data-block="true"${` data-state="${consumeCodeBlockState(lang)}"`}>${escapeHtml(token.text)}</code></pre>`;
63
72
  }
64
73
  } });
65
74
  }
@@ -98,16 +107,35 @@ var Parser = class Parser {
98
107
  getCodeBlockState(lang) {
99
108
  return this.options.codeBlockStatus[lang] ?? this.options.streamStatus;
100
109
  }
110
+ /**
111
+ * Block-lex `markdown` with the same marked configuration this parser
112
+ * renders with **and the same preprocessing** — the streaming section
113
+ * splitter uses it to decide whether a `#` line really starts a new
114
+ * top-level block, so the splitter and the renderer can never disagree.
115
+ * Lexing the raw source instead would ask the two about different documents:
116
+ * `protectCustomTags` deletes whole regions the renderer never sees, and
117
+ * `escapeRawHtml` turns tags into text, either of which moves a heading or a
118
+ * definition across a boundary. `config.extensions` (the bundled LaTeX
119
+ * plugin's `$$` rule) come along through `markdownInstance`.
120
+ */
121
+ lex(markdown) {
122
+ return this.markdownInstance.lexer(this.preprocess(markdown));
123
+ }
101
124
  parse(markdown, parseOptions) {
102
125
  this.injectTail = parseOptions?.injectTail ?? false;
103
- let processed = markdown;
104
- if (this.options.protectCustomTags) processed = this.protectCustomTags(processed);
105
- if (this.options.escapeRawHtml) processed = this.escapeRawHtml(processed);
126
+ const processed = this.preprocess(markdown);
106
127
  this.prepareCodeBlockStates(processed);
107
128
  const html = this.markedParse(processed);
108
129
  if (this.injectTail) return this.injectTailMarker(html);
109
130
  return html;
110
131
  }
132
+ /** Everything `parse` does to the source before marked sees it. */
133
+ preprocess(markdown) {
134
+ let processed = markdown;
135
+ if (this.options.protectCustomTags) processed = this.protectCustomTags(processed);
136
+ if (this.options.escapeRawHtml) processed = this.escapeRawHtml(processed);
137
+ return processed;
138
+ }
111
139
  markedParse(markdown) {
112
140
  return this.markdownInstance.parse(markdown);
113
141
  }
@@ -58,16 +58,18 @@ var VueRenderer = class {
58
58
  componentsProps: options.componentsProps ?? {},
59
59
  enableAnimation: options.enableAnimation ?? true,
60
60
  animationConfig: {
61
+ ...options.animationConfig,
61
62
  fadeDuration: options.animationConfig?.fadeDuration ?? DEFAULT_ANIMATION_DURATION,
62
63
  easing: options.animationConfig?.easing ?? "ease-in-out"
63
64
  }
64
65
  };
65
66
  }
66
67
  render(html) {
68
+ if (!html) return h(Fragment);
67
69
  const unclosedTags = detectUnclosedComponentTags(html, Object.keys(this.options.components));
68
70
  const sanitized = this.sanitize(html);
69
71
  const nodes = this.parseToVNodes(sanitized, unclosedTags);
70
- if (nodes.length === 0) return h("span", "");
72
+ if (nodes.length === 0) return h(Fragment);
71
73
  if (nodes.length === 1) return nodes[0];
72
74
  return h(Fragment, nodes);
73
75
  }
@@ -134,12 +136,12 @@ var VueRenderer = class {
134
136
  });
135
137
  }
136
138
  parseToVNodes(html, unclosedTags) {
137
- const template = `<div>${html}</div>`;
138
139
  const container = document.createElement("div");
139
- container.innerHTML = template;
140
+ container.innerHTML = `<div>${html}</div>`;
140
141
  const cidRef = { tagIndexes: {} };
141
142
  const nodes = [];
142
- Array.from(container.childNodes).forEach((node) => {
143
+ const contentParent = container.firstElementChild ?? container;
144
+ Array.from(contentParent.childNodes).forEach((node) => {
143
145
  const vnode = this.convertNode(node, unclosedTags, cidRef);
144
146
  if (vnode) nodes.push(vnode);
145
147
  });
@@ -212,10 +214,14 @@ var VueRenderer = class {
212
214
  return props;
213
215
  }
214
216
  wrapWithAnimation(text) {
217
+ const { fadeDuration, easing, splitBy, delimiters, maxSentenceChars } = this.options.animationConfig;
215
218
  return h(AnimationText_default, {
216
219
  text,
217
- fadeDuration: this.options.animationConfig.fadeDuration,
218
- easing: this.options.animationConfig.easing
220
+ fadeDuration,
221
+ easing,
222
+ splitBy,
223
+ delimiters,
224
+ maxSentenceChars
219
225
  });
220
226
  }
221
227
  setOptions(options) {
@@ -228,9 +234,10 @@ var VueRenderer = class {
228
234
  ...options.componentsProps
229
235
  };
230
236
  if (options.enableAnimation !== void 0) this.options.enableAnimation = options.enableAnimation;
231
- if (options.animationConfig) this.options.animationConfig = {
232
- ...this.options.animationConfig,
233
- ...options.animationConfig
237
+ if ("animationConfig" in options) this.options.animationConfig = {
238
+ ...options.animationConfig,
239
+ fadeDuration: options.animationConfig?.fadeDuration ?? DEFAULT_ANIMATION_DURATION,
240
+ easing: options.animationConfig?.easing ?? "ease-in-out"
234
241
  };
235
242
  }
236
243
  };
@@ -1,2 +1,14 @@
1
1
  export declare const getTagInstanceId: (tagName: string, instance: number) => string;
2
2
  export declare function detectUnclosedComponentTags(html: string, componentNames: Iterable<string>): Set<string>;
3
+ /**
4
+ * Whether `text` (a section's worth of Markdown source) leaves a raw HTML
5
+ * container open. CommonMark ends an HTML block at a blank line, but the
6
+ * browser keeps nesting into an unclosed `<div>`/`<details>`/`<table>`/…
7
+ * until its closing tag — so a section split placed before the next heading
8
+ * would auto-close the container at the section end and produce a different
9
+ * DOM from the whole-document render. An open `<p>` is the one exception:
10
+ * the HTML parser closes it when a heading starts, so both parses nest the
11
+ * heading identically. (Downstream-only guard; upstream ant-design/x#2061
12
+ * only checks registered custom component tags here.)
13
+ */
14
+ export declare function hasUnclosedRawTags(text: string): boolean;