@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.
Files changed (31) hide show
  1. package/README.md +45 -13
  2. package/dist/XMarkdown/composables/useStreaming.js +7 -7
  3. package/dist/XMarkdown/core/Parser.js +20 -11
  4. package/dist/XMarkdown/core/VueRenderer.js +18 -3
  5. package/dist/XMarkdown/index.vue_vue_type_script_setup_true_lang.js +6 -0
  6. package/dist/XMarkdown/interface.d.ts +6 -0
  7. package/package.json +2 -2
  8. package/plugins/Latex/index.js +13 -4
  9. package/dist/XMarkdown/composables/__tests__/useStreaming.test.d.ts +0 -1
  10. package/dist/XMarkdown/composables/__tests__/useStreaming.test.js +0 -118
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  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/chunks/_commonjsHelpers.D26ty3Ew.js +0 -4
  26. 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
  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/chunks/test.CBQUpOM3.js +0 -2645
  28. 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
  29. 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
  30. 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
  31. 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 | Type | Default |
109
- | ------------------------ | --------------------------------------- | --------------------------- | --------------- |
110
- | content | Markdown content to render | `string` | - |
111
- | components | Map HTML nodes to custom Vue components | `Record<string, Component>` | - |
112
- | streaming | Streaming behavior config | `StreamingOption` | - |
113
- | config | Marked parse config | `MarkedConfig` | `{ gfm: true }` |
114
- | className | Extra CSS class for root container | `string` | - |
115
- | style | Inline styles for root container | `Record<string, string>` | - |
116
- | paragraphTag | HTML tag for paragraphs | `string` | `'p'` |
117
- | openLinksInNewTab | Add `target="_blank"` to all links | `boolean` | `true` |
118
- | protectCustomTagNewlines | Preserve newlines inside custom tags | `boolean` | `true` |
119
- | escapeRawHtml | Escape raw HTML as plain text | `boolean` | `false` |
120
- | debug | Enable debug mode | `boolean` | `false` |
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
- const streamCache = ref(getInitialCache());
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.value = getInitialCache();
180
+ streamCache = getInitialCache();
181
181
  return;
182
182
  }
183
- const expectedPrefix = streamCache.value.completeMarkdown + streamCache.value.pending;
184
- if (!text.startsWith(expectedPrefix)) streamCache.value = getInitialCache();
185
- const cache = streamCache.value;
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.value = getInitialCache();
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.value = {
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: (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
  }
@@ -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.1.3",
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": "^12.0.0"
35
+ "marked": "^16.2.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/dompurify": "^3.0.5"
@@ -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 inlineRuleNonStandard = /^(?:\${1,2}([^$]{1,10000}?)\${1,2}|\\\(([\s\S]{1,10000}?)\\\)|\\\[((?:\\.|[^\\]){1,10000}?)\\\])/;
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 match = src.match(inlineRuleNonStandard);
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 = (match[1] || match[2] || match[3] || "").trim();
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,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![chart](data:image/png;base64,${"A".repeat(3e5)})\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 = `![img](data:image/png;base64,${"B".repeat(2e4)})\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