@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.
- package/dist/XMarkdown/components/AnimationText.vue.d.ts +6 -0
- package/dist/XMarkdown/components/AnimationText.vue_vue_type_script_setup_true_lang.js +58 -2
- package/dist/XMarkdown/components/Section.js +5 -0
- package/dist/XMarkdown/components/Section.vue.d.ts +76 -0
- package/dist/XMarkdown/components/Section.vue_vue_type_script_lang.js +54 -0
- package/dist/XMarkdown/composables/index.d.ts +2 -1
- package/dist/XMarkdown/composables/index.js +3 -2
- package/dist/XMarkdown/composables/useStreaming.d.ts +37 -1
- package/dist/XMarkdown/composables/useStreaming.js +449 -34
- package/dist/XMarkdown/composables/useTypewriter.d.ts +11 -0
- package/dist/XMarkdown/composables/useTypewriter.js +378 -0
- package/dist/XMarkdown/core/Parser.d.ts +15 -0
- package/dist/XMarkdown/core/Parser.js +42 -14
- package/dist/XMarkdown/core/VueRenderer.js +16 -9
- package/dist/XMarkdown/core/detectUnclosedComponentTags.d.ts +12 -0
- package/dist/XMarkdown/core/detectUnclosedComponentTags.js +240 -6
- package/dist/XMarkdown/core/index.js +1 -1
- package/dist/XMarkdown/index.d.ts +6 -2
- package/dist/XMarkdown/index.js +5 -3
- package/dist/XMarkdown/index.vue.d.ts +1 -1
- package/dist/XMarkdown/index.vue_vue_type_script_setup_true_lang.js +64 -30
- package/dist/XMarkdown/interface.d.ts +241 -14
- package/dist/XMarkdown/utils/streaming.d.ts +6 -0
- package/dist/XMarkdown/utils/streaming.js +32 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -3
- package/package.json +2 -2
- 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
|
|
50
|
-
if (
|
|
51
|
-
return
|
|
55
|
+
html(token) {
|
|
56
|
+
if (options.escapeRawHtml) return escapeHtml(token.text);
|
|
57
|
+
return token.text;
|
|
52
58
|
},
|
|
53
|
-
link
|
|
54
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
61
|
-
const lang =
|
|
62
|
-
return `<pre><code class="language-${lang}"${lang ? ` data-lang="${lang}"` : ""} data-block="true"${` data-state="${
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
140
|
+
container.innerHTML = `<div>${html}</div>`;
|
|
140
141
|
const cidRef = { tagIndexes: {} };
|
|
141
142
|
const nodes = [];
|
|
142
|
-
|
|
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
|
|
218
|
-
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
|
|
232
|
-
...
|
|
233
|
-
|
|
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;
|