@antdv-next/x-markdown 0.1.3 → 0.2.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/README.md +45 -13
- package/dist/XMarkdown/composables/useStreaming.js +7 -7
- package/dist/XMarkdown/core/Parser.js +20 -11
- package/dist/XMarkdown/core/VueRenderer.js +18 -3
- package/dist/XMarkdown/index.vue_vue_type_script_setup_true_lang.js +6 -0
- package/dist/XMarkdown/interface.d.ts +6 -0
- package/package.json +2 -2
- package/plugins/Latex/index.js +13 -4
- package/dist/XMarkdown/composables/__tests__/useStreaming.test.d.ts +0 -1
- package/dist/XMarkdown/composables/__tests__/useStreaming.test.js +0 -118
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/expect/index.js +0 -1467
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/pretty-format/index.js +0 -884
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js +0 -1513
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/snapshot/index.js +0 -661
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/spy/index.js +0 -384
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/chunk-pathe.M-eThtNZ.js +0 -80
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/diff.js +0 -1304
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/display.js +0 -556
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/error.js +0 -27
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/helpers.js +0 -179
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/offset.js +0 -25
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/serialize.js +0 -75
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/source-map.js +0 -371
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/utils/timers.js +0 -35
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/chunks/_commonjsHelpers.D26ty3Ew.js +0 -4
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/chunks/rpc.MzXet3jl.js +0 -50
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/chunks/test.CBQUpOM3.js +0 -2645
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/chunks/utils.BX5Fg8C4.js +0 -42
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/vendor/chai.js +0 -2872
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/vendor/magic-string.js +0 -1009
- package/dist/node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/vendor/tinyrainbow.js +0 -84
package/README.md
CHANGED
|
@@ -105,19 +105,51 @@ import "@antdv-next/x-markdown/themes/light.css";
|
|
|
105
105
|
|
|
106
106
|
### Props
|
|
107
107
|
|
|
108
|
-
| Property | Description
|
|
109
|
-
| ------------------------ |
|
|
110
|
-
| content | Markdown content to render
|
|
111
|
-
| components | Map HTML nodes to custom Vue components
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
108
|
+
| Property | Description | Type | Default |
|
|
109
|
+
| ------------------------ | --------------------------------------------------- | ----------------------------------------- | --------------- |
|
|
110
|
+
| content | Markdown content to render | `string` | - |
|
|
111
|
+
| components | Map HTML nodes to custom Vue components | `Record<string, Component>` | - |
|
|
112
|
+
| componentsProps | Extra props passed to custom components by tag name | `Record<string, Record<string, unknown>>` | - |
|
|
113
|
+
| streaming | Streaming behavior config | `StreamingOption` | - |
|
|
114
|
+
| config | Marked parse config | `MarkedConfig` | `{ gfm: true }` |
|
|
115
|
+
| className | Extra CSS class for root container | `string` | - |
|
|
116
|
+
| style | Inline styles for root container | `Record<string, string>` | - |
|
|
117
|
+
| paragraphTag | HTML tag for paragraphs | `string` | `'p'` |
|
|
118
|
+
| openLinksInNewTab | Add `target="_blank"` to all links | `boolean` | `true` |
|
|
119
|
+
| protectCustomTagNewlines | Preserve newlines inside custom tags | `boolean` | `true` |
|
|
120
|
+
| escapeRawHtml | Escape raw HTML as plain text | `boolean` | `false` |
|
|
121
|
+
| debug | Enable debug mode | `boolean` | `false` |
|
|
122
|
+
|
|
123
|
+
### Passing extra props to custom components
|
|
124
|
+
|
|
125
|
+
Custom components often need business data (theme, callbacks, etc.). Passing it via an inline wrapper function creates a new component on every render and forces the subtree to be re-created, losing internal state and hurting performance in streaming scenarios. Use `componentsProps` to pass extra props while keeping component references stable:
|
|
126
|
+
|
|
127
|
+
```vue
|
|
128
|
+
<script setup>
|
|
129
|
+
import { computed, ref } from "vue";
|
|
130
|
+
import { XMarkdown } from "@antdv-next/x-markdown";
|
|
131
|
+
|
|
132
|
+
const theme = ref("dark");
|
|
133
|
+
const components = { "custom-chart": CustomChart };
|
|
134
|
+
// Keep the reference stable — an inline object literal invalidates the render
|
|
135
|
+
// cache and re-parses the whole tree on every render.
|
|
136
|
+
const componentsProps = computed(() => ({
|
|
137
|
+
"custom-chart": { theme: theme.value, onSelect },
|
|
138
|
+
}));
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<template>
|
|
142
|
+
<XMarkdown :components="components" :componentsProps="componentsProps" />
|
|
143
|
+
</template>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`componentsProps` is keyed by tag name. Its props are merged with the parsed HTML attributes and passed to the component:
|
|
147
|
+
|
|
148
|
+
- On conflict `componentsProps` wins — a `title` in `componentsProps` overrides `title="..."` from the HTML.
|
|
149
|
+
- `class` / `className` is the exception: both sides are concatenated, with the `componentsProps` class name first (merged into `class`).
|
|
150
|
+
- Internally computed props — `streamStatus`, `domNode`, `children` (plus `lang` and `block` for `code`) — cannot be overridden and are ignored if present in `componentsProps`.
|
|
151
|
+
|
|
152
|
+
When `componentsProps` changes, the component receives a normal props update without being remounted. Like `components`, it takes part in the render cache, so passing an inline object literal invalidates that cache on every render and re-parses the whole tree — keep the reference stable (e.g. with `computed`) as shown above.
|
|
121
153
|
|
|
122
154
|
### StreamingOption
|
|
123
155
|
|
|
@@ -163,7 +163,7 @@ var safeEncodeURIComponent = (str) => {
|
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
165
|
function useStreaming(content, streaming, components) {
|
|
166
|
-
|
|
166
|
+
let streamCache = getInitialCache();
|
|
167
167
|
const processedContent = ref("");
|
|
168
168
|
function handleIncompleteMarkdown(cache, opts) {
|
|
169
169
|
const { token, pending } = cache;
|
|
@@ -177,12 +177,12 @@ function useStreaming(content, streaming, components) {
|
|
|
177
177
|
function processStreaming(text, opts) {
|
|
178
178
|
if (!text) {
|
|
179
179
|
processedContent.value = "";
|
|
180
|
-
streamCache
|
|
180
|
+
streamCache = getInitialCache();
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
|
-
const expectedPrefix = streamCache.
|
|
184
|
-
if (!text.startsWith(expectedPrefix)) streamCache
|
|
185
|
-
const cache = streamCache
|
|
183
|
+
const expectedPrefix = streamCache.completeMarkdown + streamCache.pending;
|
|
184
|
+
if (!text.startsWith(expectedPrefix)) streamCache = getInitialCache();
|
|
185
|
+
const cache = streamCache;
|
|
186
186
|
const chunk = text.slice(cache.processedLength);
|
|
187
187
|
if (!chunk) {
|
|
188
188
|
const incompletePlaceholder = handleIncompleteMarkdown(cache, opts);
|
|
@@ -209,14 +209,14 @@ function useStreaming(content, streaming, components) {
|
|
|
209
209
|
processedContent.value = cache.completeMarkdown + (incompletePlaceholder || "");
|
|
210
210
|
}
|
|
211
211
|
function reset() {
|
|
212
|
-
streamCache
|
|
212
|
+
streamCache = getInitialCache();
|
|
213
213
|
processedContent.value = "";
|
|
214
214
|
}
|
|
215
215
|
watch([content, () => streaming.value?.hasNextChunk], ([newContent, hasNextChunk]) => {
|
|
216
216
|
const opts = streaming.value;
|
|
217
217
|
if (!Boolean(hasNextChunk)) {
|
|
218
218
|
processedContent.value = newContent;
|
|
219
|
-
streamCache
|
|
219
|
+
streamCache = {
|
|
220
220
|
pending: "",
|
|
221
221
|
token: StreamCacheTokenType.Text,
|
|
222
222
|
processedLength: newContent.length,
|
|
@@ -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
|
}
|
|
@@ -55,6 +55,7 @@ var VueRenderer = class {
|
|
|
55
55
|
constructor(options = {}) {
|
|
56
56
|
this.options = {
|
|
57
57
|
components: options.components ?? {},
|
|
58
|
+
componentsProps: options.componentsProps ?? {},
|
|
58
59
|
enableAnimation: options.enableAnimation ?? true,
|
|
59
60
|
animationConfig: {
|
|
60
61
|
fadeDuration: options.animationConfig?.fadeDuration ?? DEFAULT_ANIMATION_DURATION,
|
|
@@ -183,12 +184,17 @@ var VueRenderer = class {
|
|
|
183
184
|
}
|
|
184
185
|
extractComponentProps(element, tagName, unclosedTags, cidRef) {
|
|
185
186
|
const props = {};
|
|
186
|
-
cidRef.tagIndexes[tagName] = (cidRef.tagIndexes[tagName] ?? 0) + 1;
|
|
187
|
-
const instanceId = getTagInstanceId(tagName, cidRef.tagIndexes[tagName]);
|
|
188
|
-
props.streamStatus = unclosedTags.has(instanceId) ? "loading" : "done";
|
|
189
187
|
Array.from(element.attributes).forEach((attr) => {
|
|
190
188
|
props[attr.name] = attr.value;
|
|
191
189
|
});
|
|
190
|
+
const htmlClass = typeof props.class === "string" ? props.class : "";
|
|
191
|
+
const extraProps = this.options.componentsProps[tagName] ?? {};
|
|
192
|
+
Object.assign(props, extraProps);
|
|
193
|
+
cidRef.tagIndexes[tagName] = (cidRef.tagIndexes[tagName] ?? 0) + 1;
|
|
194
|
+
const instanceId = getTagInstanceId(tagName, cidRef.tagIndexes[tagName]);
|
|
195
|
+
props.streamStatus = unclosedTags.has(instanceId) ? "loading" : "done";
|
|
196
|
+
props.domNode = element;
|
|
197
|
+
delete props.children;
|
|
192
198
|
if (tagName === "code") {
|
|
193
199
|
props.block = element.getAttribute("data-block") === "true";
|
|
194
200
|
props.streamStatus = element.getAttribute("data-state") === "loading" ? "loading" : "done";
|
|
@@ -197,7 +203,12 @@ var VueRenderer = class {
|
|
|
197
203
|
const langFromClass = className.match(/(?:^|\s)language-([^\s]+)/)?.[1] ?? className.match(/(?:^|\s)lang-([^\s]+)/)?.[1] ?? "";
|
|
198
204
|
const lang = langFromData || langFromClass;
|
|
199
205
|
if (lang) props.lang = lang;
|
|
206
|
+
else delete props.lang;
|
|
200
207
|
}
|
|
208
|
+
const extraClass = extraProps.className ?? extraProps.classname ?? extraProps.class;
|
|
209
|
+
if (typeof extraClass === "string") props.class = [extraClass, htmlClass].filter(Boolean).join(" ").trim();
|
|
210
|
+
delete props.className;
|
|
211
|
+
delete props.classname;
|
|
201
212
|
return props;
|
|
202
213
|
}
|
|
203
214
|
wrapWithAnimation(text) {
|
|
@@ -212,6 +223,10 @@ var VueRenderer = class {
|
|
|
212
223
|
...this.options.components,
|
|
213
224
|
...options.components
|
|
214
225
|
};
|
|
226
|
+
if (options.componentsProps) this.options.componentsProps = {
|
|
227
|
+
...this.options.componentsProps,
|
|
228
|
+
...options.componentsProps
|
|
229
|
+
};
|
|
215
230
|
if (options.enableAnimation !== void 0) this.options.enableAnimation = options.enableAnimation;
|
|
216
231
|
if (options.animationConfig) this.options.animationConfig = {
|
|
217
232
|
...this.options.animationConfig,
|
|
@@ -11,6 +11,7 @@ var index_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComp
|
|
|
11
11
|
props: {
|
|
12
12
|
content: { default: "" },
|
|
13
13
|
components: { default: () => ({}) },
|
|
14
|
+
componentsProps: {},
|
|
14
15
|
streaming: { default: void 0 },
|
|
15
16
|
config: { default: () => ({ gfm: true }) },
|
|
16
17
|
debug: {
|
|
@@ -75,6 +76,7 @@ var index_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComp
|
|
|
75
76
|
}));
|
|
76
77
|
const renderer = shallowRef(new VueRenderer({
|
|
77
78
|
components: mergedComponents.value,
|
|
79
|
+
componentsProps: props.componentsProps,
|
|
78
80
|
enableAnimation: props.streaming?.enableAnimation ?? true,
|
|
79
81
|
animationConfig: props.streaming?.animationConfig
|
|
80
82
|
}));
|
|
@@ -116,6 +118,10 @@ var index_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComp
|
|
|
116
118
|
renderer.value.setOptions({ components: newComponents });
|
|
117
119
|
bumpOptionsVersion();
|
|
118
120
|
}, { deep: true });
|
|
121
|
+
watch(() => props.componentsProps, (newComponentsProps) => {
|
|
122
|
+
renderer.value.setOptions({ componentsProps: newComponentsProps });
|
|
123
|
+
bumpOptionsVersion();
|
|
124
|
+
}, { deep: true });
|
|
119
125
|
watch(() => props.streaming, (newStreaming) => {
|
|
120
126
|
renderer.value.setOptions({
|
|
121
127
|
enableAnimation: newStreaming?.enableAnimation ?? true,
|
|
@@ -45,6 +45,11 @@ export interface StreamingOption {
|
|
|
45
45
|
export interface XMarkdownProps {
|
|
46
46
|
content?: string;
|
|
47
47
|
components?: Record<string, Component>;
|
|
48
|
+
/**
|
|
49
|
+
* 按标签名向 `components` 中的自定义组件传递额外的 props,使组件引用保持稳定,避免内联函数导致的重复挂载
|
|
50
|
+
* Extra props passed to custom components in `components` by tag name, keeping component references stable and avoiding remounts caused by inline functions
|
|
51
|
+
*/
|
|
52
|
+
componentsProps?: Record<string, Record<string, unknown>>;
|
|
48
53
|
streaming?: StreamingOption;
|
|
49
54
|
config?: MarkedConfig;
|
|
50
55
|
debug?: boolean;
|
|
@@ -91,6 +96,7 @@ export interface ParserOptions {
|
|
|
91
96
|
}
|
|
92
97
|
export interface RendererOptions {
|
|
93
98
|
components?: Record<string, Component>;
|
|
99
|
+
componentsProps?: Record<string, Record<string, unknown>>;
|
|
94
100
|
enableAnimation?: boolean;
|
|
95
101
|
animationConfig?: {
|
|
96
102
|
fadeDuration?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antdv-next/x-markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"dompurify": "^3.1.0",
|
|
34
34
|
"katex": "^0.16.25",
|
|
35
|
-
"marked": "^
|
|
35
|
+
"marked": "^16.2.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/dompurify": "^3.0.5"
|
package/plugins/Latex/index.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import katex from "katex";
|
|
2
2
|
import "katex/dist/katex.min.css";
|
|
3
3
|
//#region src/plugins/Latex/index.ts
|
|
4
|
-
var
|
|
4
|
+
var inlineDollarRule = /^(\${1,2})((?:\\.|[^\\$]){1,10000}?)\1/;
|
|
5
|
+
var inlineRuleNonStandard = /^(?:\\\(([\s\S]{1,10000}?)\\\)|\\\[((?:\\.|[^\\]){1,10000}?)\\\])/;
|
|
5
6
|
var blockRule = /^(\${1,2})\n([\s\S]{1,10000}?)\n\1(?:\s*(?:\n|$))|^\\\[((?:\\.|[^\\]){1,10000}?)\\\]/;
|
|
6
7
|
function replaceAlign(text) {
|
|
7
8
|
return text ? text.replace(/\{align\*\}/g, "{aligned}") : text;
|
|
8
9
|
}
|
|
10
|
+
function isValidInlineDollarMatch(match, src) {
|
|
11
|
+
const [, delimiter, text] = match;
|
|
12
|
+
if (delimiter === "$$") return true;
|
|
13
|
+
return !/^\s|\s$/.test(text) && !/^\d/.test(src.slice(match[0].length));
|
|
14
|
+
}
|
|
9
15
|
function createRenderer(options, newlineAfter) {
|
|
10
16
|
return (token) => katex.renderToString(token.text, {
|
|
11
17
|
...options,
|
|
@@ -25,10 +31,13 @@ function inlineKatex(renderer, replaceAlignStart) {
|
|
|
25
31
|
return indices.length > 0 ? Math.min(...indices) : void 0;
|
|
26
32
|
},
|
|
27
33
|
tokenizer(src) {
|
|
28
|
-
const
|
|
34
|
+
const dollarMatch = src.match(inlineDollarRule);
|
|
35
|
+
if (dollarMatch && !isValidInlineDollarMatch(dollarMatch, src)) return;
|
|
36
|
+
const nonStandardMatch = src.match(inlineRuleNonStandard);
|
|
37
|
+
const match = dollarMatch || nonStandardMatch;
|
|
29
38
|
if (!match) return;
|
|
30
|
-
const rawText =
|
|
31
|
-
const text = replaceAlignStart ? replaceAlign(rawText) : rawText;
|
|
39
|
+
const rawText = dollarMatch?.[2] || nonStandardMatch?.[1] || nonStandardMatch?.[2] || "";
|
|
40
|
+
const text = replaceAlignStart ? replaceAlign(rawText.trim()) : rawText.trim();
|
|
32
41
|
return {
|
|
33
42
|
type: "inlineKatex",
|
|
34
43
|
raw: match[0],
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { h as describe, m as it } from "../../../node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js";
|
|
2
|
-
import { g as globalExpect } from "../../../node_modules/.pnpm/@voidzero-dev_vite-plus-test@0.1.14_@types_node@24.12.0_@voidzero-dev_vite-plus-core@0._0c620f79340a656256b2ffe23b3e6dd1/node_modules/@voidzero-dev/vite-plus-test/dist/chunks/test.CBQUpOM3.js";
|
|
3
|
-
import { useStreaming } from "../useStreaming.js";
|
|
4
|
-
import { effectScope, nextTick, ref } from "vue";
|
|
5
|
-
//#region src/XMarkdown/composables/__tests__/useStreaming.test.ts
|
|
6
|
-
/**
|
|
7
|
-
* Drive `useStreaming` with streaming enabled and return the latest
|
|
8
|
-
* `processedContent` value after Vue flushes.
|
|
9
|
-
*/
|
|
10
|
-
async function stream(chunks, opts = {}) {
|
|
11
|
-
let result = "";
|
|
12
|
-
const scope = effectScope();
|
|
13
|
-
await scope.run(async () => {
|
|
14
|
-
const content = ref("");
|
|
15
|
-
const { processedContent } = useStreaming(content, ref({ hasNextChunk: opts.hasNextChunk ?? true }));
|
|
16
|
-
await nextTick();
|
|
17
|
-
let acc = "";
|
|
18
|
-
for (const chunk of chunks) {
|
|
19
|
-
acc += chunk;
|
|
20
|
-
content.value = acc;
|
|
21
|
-
await nextTick();
|
|
22
|
-
}
|
|
23
|
-
result = processedContent.value;
|
|
24
|
-
});
|
|
25
|
-
scope.stop();
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
describe("useStreaming fence correctness", () => {
|
|
29
|
-
it("streams plain text unchanged", async () => {
|
|
30
|
-
globalExpect(await stream(["Hello", " world"])).toBe("Hello world");
|
|
31
|
-
});
|
|
32
|
-
it("opens a fence on a partial last line immediately", async () => {
|
|
33
|
-
globalExpect(await stream(["```js\nconst a = 1;\n"])).toBe("```js\nconst a = 1;\n");
|
|
34
|
-
});
|
|
35
|
-
it("keeps an unclosed fence open for streaming continuation", async () => {
|
|
36
|
-
globalExpect(await stream(["```\ncode here"])).toBe("```\ncode here");
|
|
37
|
-
});
|
|
38
|
-
it("closes a fence once its line is completed by a newline", async () => {
|
|
39
|
-
globalExpect(await stream(["```\ncode\n```\nafter"])).toBe("```\ncode\n```\nafter");
|
|
40
|
-
});
|
|
41
|
-
it("requires the same fence char to close", async () => {
|
|
42
|
-
globalExpect(await stream(["~~~\ncode\n```\nstill in fence"])).toBe("~~~\ncode\n```\nstill in fence");
|
|
43
|
-
});
|
|
44
|
-
it("requires at least the opening run length to close", async () => {
|
|
45
|
-
globalExpect(await stream(["````\ncode\n```\nstill in fence"])).toBe("````\ncode\n```\nstill in fence");
|
|
46
|
-
});
|
|
47
|
-
it("allows a longer closing fence", async () => {
|
|
48
|
-
globalExpect(await stream(["```\ncode\n````\nafter"])).toBe("```\ncode\n````\nafter");
|
|
49
|
-
});
|
|
50
|
-
it("treats a closing fence with trailing whitespace as valid", async () => {
|
|
51
|
-
globalExpect(await stream(["```\ncode\n``` \nafter"])).toBe("```\ncode\n``` \nafter");
|
|
52
|
-
});
|
|
53
|
-
it("does not treat a closing fence with non-whitespace tail as close", async () => {
|
|
54
|
-
globalExpect(await stream(["```\ncode\n```js\nstill in fence"])).toBe("```\ncode\n```js\nstill in fence");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
describe("useStreaming CRLF semantics", () => {
|
|
58
|
-
it("handles CRLF fence open and close", async () => {
|
|
59
|
-
globalExpect(await stream(["```\r\ncode\r\n```\r\nafter"])).toBe("```\r\ncode\r\n```\r\nafter");
|
|
60
|
-
});
|
|
61
|
-
it("handles a trailing CR", async () => {
|
|
62
|
-
globalExpect(await stream(["```\r"])).toBe("```\r");
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
describe("useStreaming long single-line content (base64 images)", () => {
|
|
66
|
-
it("streams a large base64 image without quadratic slowdown", async () => {
|
|
67
|
-
const full = `Here is an image:\n\n})\n\nDone.`;
|
|
68
|
-
const chunkSize = 1e3;
|
|
69
|
-
let result = "";
|
|
70
|
-
const scope = effectScope();
|
|
71
|
-
const start = performance.now();
|
|
72
|
-
await scope.run(async () => {
|
|
73
|
-
const content = ref("");
|
|
74
|
-
const { processedContent } = useStreaming(content, ref({ hasNextChunk: true }));
|
|
75
|
-
await nextTick();
|
|
76
|
-
let acc = "";
|
|
77
|
-
for (let end = chunkSize; end < full.length + chunkSize; end += chunkSize) {
|
|
78
|
-
acc = full.slice(0, Math.min(end, full.length));
|
|
79
|
-
content.value = acc;
|
|
80
|
-
await nextTick();
|
|
81
|
-
}
|
|
82
|
-
result = processedContent.value;
|
|
83
|
-
});
|
|
84
|
-
const elapsed = performance.now() - start;
|
|
85
|
-
scope.stop();
|
|
86
|
-
globalExpect(result).toBe(full);
|
|
87
|
-
globalExpect(elapsed).toBeLessThan(1e4);
|
|
88
|
-
}, 12e4);
|
|
89
|
-
it("keeps fence state correct when a code block follows long content", async () => {
|
|
90
|
-
const full = `})\n\n\`\`\`js\nconst a = 1;\n`;
|
|
91
|
-
let result = "";
|
|
92
|
-
const scope = effectScope();
|
|
93
|
-
await scope.run(async () => {
|
|
94
|
-
const content = ref(full.slice(0, 100));
|
|
95
|
-
const { processedContent } = useStreaming(content, ref({ hasNextChunk: true }));
|
|
96
|
-
await nextTick();
|
|
97
|
-
content.value = full;
|
|
98
|
-
await nextTick();
|
|
99
|
-
result = processedContent.value;
|
|
100
|
-
});
|
|
101
|
-
scope.stop();
|
|
102
|
-
globalExpect(result).toBe(full);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
describe("useStreaming non-streaming passthrough", () => {
|
|
106
|
-
it("returns content verbatim when hasNextChunk is false", async () => {
|
|
107
|
-
let result = "";
|
|
108
|
-
const scope = effectScope();
|
|
109
|
-
await scope.run(async () => {
|
|
110
|
-
const { processedContent } = useStreaming(ref("```\ncode\n```\ntext"), ref({ hasNextChunk: false }));
|
|
111
|
-
await nextTick();
|
|
112
|
-
result = processedContent.value;
|
|
113
|
-
});
|
|
114
|
-
scope.stop();
|
|
115
|
-
globalExpect(result).toBe("```\ncode\n```\ntext");
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
//#endregion
|