@preferred-markdown-stream/vue 0.1.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 ADDED
@@ -0,0 +1,32 @@
1
+ # @preferred-markdown-stream/vue
2
+
3
+ Vue bindings and runtime helpers for streaming Markdown rendering.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @preferred-markdown-stream/vue vue
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import {
15
+ createStreamingMarkdownVNodes,
16
+ createVNodeRendererComponent,
17
+ } from '@preferred-markdown-stream/vue'
18
+ ```
19
+
20
+ ## Public API
21
+
22
+ - `addFadeInToVNodes(children, loading, fadeInClass?)`
23
+ - `createStreamingMarkdownVNodes(content, loading)`
24
+ - `createVNodeRendererComponent(vnodes)`
25
+ - `setCodeBlockComponent(component)`
26
+
27
+ ## Notes
28
+
29
+ - The package currently preserves the app's existing runtime behavior.
30
+ - `fadeIn.ts` adapts the framework-agnostic tree animation helper for Vue VNodes.
31
+ - `runtime.ts` stays internal and owns the Markdown renderer instance plus lazy capability loading.
32
+ - `useStreamingMarkdown.ts` contains the repeated Vue wiring that was previously duplicated inside app components.
@@ -0,0 +1,2 @@
1
+ import type { VNode } from 'vue';
2
+ export declare function addFadeInToVNodes(childrenRaw: VNode[], loading: boolean, fadeInClass?: string): VNode[];
package/dist/fadeIn.js ADDED
@@ -0,0 +1,4 @@
1
+ import { addFadeInClassToTreeNodes } from '@preferred-markdown-stream/core';
2
+ export function addFadeInToVNodes(childrenRaw, loading, fadeInClass = 'fade-in') {
3
+ return addFadeInClassToTreeNodes(childrenRaw, loading, fadeInClass);
4
+ }
@@ -0,0 +1,3 @@
1
+ export { addFadeInToVNodes, } from './fadeIn.js';
2
+ export { setCodeBlockComponent, } from './render.js';
3
+ export { createStreamingMarkdownVNodes, createVNodeRendererComponent, } from './useStreamingMarkdown.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { addFadeInToVNodes, } from './fadeIn.js';
2
+ export { setCodeBlockComponent, } from './render.js';
3
+ export { createStreamingMarkdownVNodes, createVNodeRendererComponent, } from './useStreamingMarkdown.js';
@@ -0,0 +1,17 @@
1
+ export interface Plugin<Ctx = any> {
2
+ name: string;
3
+ register?: (ctx: Ctx) => any;
4
+ }
5
+ export declare const DOM_ATTR_NAME: {
6
+ SOURCE_LINE_START: string;
7
+ SOURCE_LINE_END: string;
8
+ ORIGIN_SRC: string;
9
+ ORIGIN_HREF: string;
10
+ LOCAL_IMAGE: string;
11
+ ONLY_CHILD: string;
12
+ TOKEN_IDX: string;
13
+ DISPLAY_NONE: string;
14
+ };
15
+ export declare function setCodeBlockComponent(component: any): void;
16
+ declare function render_(md: any): void;
17
+ export default render_;
package/dist/render.js ADDED
@@ -0,0 +1,326 @@
1
+ import { escapeHtml, unescapeAll } from 'markdown-it/lib/common/utils.mjs';
2
+ import { Comment, createVNode, Fragment, h, Text, } from 'vue';
3
+ export const DOM_ATTR_NAME = {
4
+ SOURCE_LINE_START: 'data-source-line',
5
+ SOURCE_LINE_END: 'data-source-line-end',
6
+ ORIGIN_SRC: 'origin-src',
7
+ ORIGIN_HREF: 'origin-href',
8
+ LOCAL_IMAGE: 'local-image',
9
+ ONLY_CHILD: 'auto-center',
10
+ TOKEN_IDX: 'data-token-idx',
11
+ DISPLAY_NONE: 'display-none',
12
+ };
13
+ const sensitiveUrlReg = /^javascript:|vbscript:|file:/i;
14
+ const sensitiveAttrReg = /^href|src|xlink:href|poster/i;
15
+ const attrNameReg = /^[a-z_:][\w:.-]*$/i;
16
+ const attrEventReg = /^on/i;
17
+ const CODE_INFO_SPLIT_REGEXP = /(\s+)/g;
18
+ const STREAMING_TEXT_SPLIT_REGEXP = /(?<=[。?!;、,,;\n])|(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=[.?!`])/g;
19
+ const defaultRules = {};
20
+ function validateAttrName(name) {
21
+ return attrNameReg.test(name) && !attrEventReg.test(name);
22
+ }
23
+ function getLine(token, env) {
24
+ const [lineStart, lineEnd] = token.map || [0, 1];
25
+ // macro, calc line offset, see `markdown-macro` plugin.
26
+ let sOffset = 0;
27
+ if (env?.macroLines && env.bMarks && env.eMarks) {
28
+ const sPos = env.bMarks[lineStart];
29
+ for (let i = 0; i < env.macroLines.length; i++) {
30
+ const { matchPos, lineOffset, posOffset, currentPosOffset } = env.macroLines[i];
31
+ if (sPos + posOffset > matchPos
32
+ && sPos + posOffset - currentPosOffset > matchPos) {
33
+ sOffset = lineOffset;
34
+ }
35
+ else {
36
+ break;
37
+ }
38
+ }
39
+ }
40
+ return [lineStart + sOffset, lineEnd + sOffset];
41
+ }
42
+ function processToken(token, env) {
43
+ if (!token.meta) {
44
+ token.meta = {};
45
+ }
46
+ if (env?.safeMode && token.attrs) {
47
+ for (let [name, val] of token.attrs) {
48
+ name = name.toLowerCase();
49
+ if (sensitiveAttrReg.test(name) && sensitiveUrlReg.test(val)) {
50
+ token.attrSet(name, '');
51
+ }
52
+ if (name === 'href' && val.toLowerCase().startsWith('data:')) {
53
+ token.attrSet(name, '');
54
+ }
55
+ }
56
+ }
57
+ if (token.block) {
58
+ const [lineStart, lineEnd] = getLine(token, env);
59
+ if (token.map) {
60
+ token.attrSet(DOM_ATTR_NAME.SOURCE_LINE_START, String(lineStart + 1));
61
+ token.attrSet(DOM_ATTR_NAME.SOURCE_LINE_END, String(lineEnd + 1));
62
+ if (!token.meta.attrs) {
63
+ token.meta.attrs = {};
64
+ }
65
+ // transform array to object
66
+ if (token.attrs) {
67
+ for (const [name, val] of token.attrs) {
68
+ token.meta.attrs[name] = val;
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ defaultRules.code_inline = function (tokens, idx, _, __, slf) {
75
+ const token = tokens[idx];
76
+ return createVNode('code', { ...slf.renderAttrs(token), key: idx }, token.content);
77
+ };
78
+ defaultRules.code_block = function (tokens, idx, _, __, slf) {
79
+ const token = tokens[idx];
80
+ const attrs = slf.renderAttrs(token);
81
+ const preAttrs = {
82
+ [DOM_ATTR_NAME.SOURCE_LINE_START]: attrs[DOM_ATTR_NAME.SOURCE_LINE_START],
83
+ [DOM_ATTR_NAME.SOURCE_LINE_END]: attrs[DOM_ATTR_NAME.SOURCE_LINE_END],
84
+ };
85
+ delete attrs[DOM_ATTR_NAME.SOURCE_LINE_START];
86
+ delete attrs[DOM_ATTR_NAME.SOURCE_LINE_END];
87
+ return h('pre', preAttrs, [
88
+ h('code', attrs, [createVNode(Text, {}, token.content)]),
89
+ ]);
90
+ };
91
+ /**
92
+ * Custom code block component slot.
93
+ * When set via `setCodeBlockComponent()`, the fence rule delegates rendering
94
+ * to this component instead of the default `pre > code`.
95
+ *
96
+ * The component receives props: `{ language: string, content: string, preAttrs: Record<string, any> }`
97
+ * where `content` is the highlighted HTML string.
98
+ */
99
+ let customCodeBlockComponent = null;
100
+ export function setCodeBlockComponent(component) {
101
+ customCodeBlockComponent = component;
102
+ }
103
+ // fence rule
104
+ defaultRules.fence = function (tokens, idx, options, _, slf) {
105
+ const token = tokens[idx];
106
+ const info = token.info ? unescapeAll(token.info).trim() : '';
107
+ let langName = '';
108
+ let langAttrs = '';
109
+ let i;
110
+ let arr;
111
+ let tmpAttrs;
112
+ let tmpToken;
113
+ if (info) {
114
+ arr = info.split(CODE_INFO_SPLIT_REGEXP);
115
+ langName = arr[0];
116
+ langAttrs = arr.slice(2).join('');
117
+ }
118
+ const highlighted = options.highlight
119
+ ? options.highlight(token.content, langName, langAttrs)
120
+ || escapeHtml(token.content)
121
+ : escapeHtml(token.content);
122
+ const buildVNode = (attrs) => {
123
+ const preAttrs = {
124
+ 'data-info': info,
125
+ 'data-lang': langName,
126
+ [DOM_ATTR_NAME.SOURCE_LINE_START]: attrs[DOM_ATTR_NAME.SOURCE_LINE_START],
127
+ [DOM_ATTR_NAME.SOURCE_LINE_END]: attrs[DOM_ATTR_NAME.SOURCE_LINE_END],
128
+ };
129
+ delete attrs[DOM_ATTR_NAME.SOURCE_LINE_START];
130
+ delete attrs[DOM_ATTR_NAME.SOURCE_LINE_END];
131
+ if (customCodeBlockComponent) {
132
+ return h(customCodeBlockComponent, {
133
+ language: langName,
134
+ content: highlighted,
135
+ preAttrs,
136
+ });
137
+ }
138
+ // Default: simple pre > code
139
+ return h('pre', preAttrs, [
140
+ createVNode('code', { innerHTML: highlighted }, []),
141
+ ]);
142
+ };
143
+ // If language exists, inject class gently, without modifying original token.
144
+ if (info) {
145
+ i = token.attrIndex('class');
146
+ tmpAttrs = token.attrs ? [...token.attrs] : [];
147
+ if (i < 0) {
148
+ tmpAttrs.push(['class', options.langPrefix + langName]);
149
+ }
150
+ else {
151
+ tmpAttrs[i] = [...tmpAttrs[i]];
152
+ tmpAttrs[i][1] += ` ${options.langPrefix}${langName}`;
153
+ }
154
+ // Fake token just to render attributes
155
+ tmpToken = {
156
+ attrs: tmpAttrs,
157
+ };
158
+ return buildVNode(slf.renderAttrs(tmpToken));
159
+ }
160
+ return buildVNode(slf.renderAttrs(token));
161
+ };
162
+ defaultRules.image = function (tokens, idx, options, env, slf) {
163
+ const token = tokens[idx];
164
+ return h('img', {
165
+ ...slf.renderAttrs(token),
166
+ alt: slf.renderInlineAsText(token.children || [], options, env),
167
+ }, []);
168
+ };
169
+ defaultRules.hardbreak = function (_, idx) {
170
+ return h('br', { key: idx });
171
+ };
172
+ defaultRules.softbreak = function (_, idx, options) {
173
+ return options.breaks ? h('br', { key: idx }) : null;
174
+ };
175
+ defaultRules.list_item_open = function (tokens, idx, _, __, self) {
176
+ return h('li', { key: idx, ...self.renderAttrs(tokens[idx]) }, []);
177
+ };
178
+ defaultRules.paragraph_open = function (tokens, idx, _, __, self) {
179
+ // 强制添加 p 标签,即使单个列表项。从而防止列表项元素突变。
180
+ return h('p', { key: idx, ...self.renderAttrs(tokens[idx]) }, []);
181
+ };
182
+ defaultRules.text = function (tokens, idx, _, env) {
183
+ const token = tokens[idx];
184
+ if (env.sanitize) {
185
+ // 检查 content 是否包含是句尾符号,如 ',', '。', '?', '!', '. '
186
+ // 使用
187
+ // 分割英语 (?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)
188
+ // 分割中文 (?<=[。?!;])
189
+ const splited = token.content.split(STREAMING_TEXT_SPLIT_REGEXP);
190
+ return splited.map((content, i) => {
191
+ return createVNode('span', { key: i }, { default: () => content });
192
+ });
193
+ }
194
+ else {
195
+ // console.log('text', token)
196
+ return createVNode(Text, { key: idx }, token.content);
197
+ // return createVNode(Text, { key: idx }, token.content)
198
+ }
199
+ };
200
+ defaultRules.html_block = function (tokens, idx) {
201
+ const token = tokens[idx];
202
+ if (token.contentVNode) {
203
+ return token.contentVNode;
204
+ }
205
+ return createHtmlVNode(token.content);
206
+ };
207
+ defaultRules.html_inline = function (tokens, idx) {
208
+ const token = tokens[idx];
209
+ if (token.contentVNode) {
210
+ return token.contentVNode;
211
+ }
212
+ return createHtmlVNode(token.content);
213
+ };
214
+ function createHtmlVNode(html) {
215
+ const div = document.createElement('template');
216
+ div.innerHTML = html;
217
+ const elements = div.content.children;
218
+ const children = [];
219
+ for (const element of elements) {
220
+ const tagName = element.tagName.toLowerCase();
221
+ const attrs = {
222
+ key: element.outerHTML,
223
+ };
224
+ for (let j = 0; j < element.attributes.length; j++) {
225
+ const attr = element.attributes[j];
226
+ attrs[attr.name] = attr.value;
227
+ }
228
+ attrs.innerHTML = element.innerHTML;
229
+ attrs.key = element.innerHTML;
230
+ children.push(h(tagName, attrs, []));
231
+ }
232
+ return h(Fragment, {}, children);
233
+ }
234
+ function renderToken(tokens, idx) {
235
+ const token = tokens[idx];
236
+ if (token.nesting === -1) {
237
+ return null;
238
+ }
239
+ // Tight list paragraphs
240
+ if (token.hidden) {
241
+ return createVNode(Fragment, {}, []);
242
+ }
243
+ if (token.tag === '--') {
244
+ return createVNode(Comment);
245
+ }
246
+ return createVNode(token.tag, this.renderAttrs(token), []);
247
+ }
248
+ function renderAttrs(token) {
249
+ if (!token.attrs) {
250
+ return {};
251
+ }
252
+ const result = {};
253
+ // eslint-disable-next-line unicorn/no-array-for-each
254
+ token.attrs.forEach((token) => {
255
+ if (validateAttrName(token[0])) {
256
+ result[token[0]] = token[1];
257
+ }
258
+ });
259
+ return result;
260
+ }
261
+ function render(tokens, options, env) {
262
+ const rules = this.rules;
263
+ const vNodeParents = [];
264
+ return tokens
265
+ .map((token, i) => {
266
+ processToken(token, env);
267
+ if (token.block) {
268
+ token.attrSet(DOM_ATTR_NAME.TOKEN_IDX, i.toString());
269
+ }
270
+ const { type } = token;
271
+ let vnode = null;
272
+ let parent = null;
273
+ if (type === 'inline') {
274
+ vnode = this.render(token.children || [], options, env);
275
+ }
276
+ else if (rules[type]) {
277
+ const result = rules[type](tokens, i, options, env, this);
278
+ if (typeof result === 'string') {
279
+ vnode = createHtmlVNode(result);
280
+ }
281
+ else if (result && result.node && result.parent) {
282
+ parent = result.parent;
283
+ vnode = result.node;
284
+ }
285
+ else {
286
+ vnode = result;
287
+ }
288
+ }
289
+ else {
290
+ vnode = this.renderToken(tokens, i, options);
291
+ }
292
+ let isChild = false;
293
+ const parentNode = vNodeParents.length > 0 ? vNodeParents.at(-1) : null;
294
+ if (vnode && parentNode) {
295
+ if (typeof parentNode.type === 'string'
296
+ || parentNode.type === Fragment) {
297
+ const children = Array.isArray(parentNode.children)
298
+ ? parentNode.children
299
+ : [];
300
+ parentNode.children = [...children, vnode];
301
+ }
302
+ isChild = true;
303
+ }
304
+ if (token.nesting === 1) {
305
+ if (parent) {
306
+ vNodeParents.push(parent);
307
+ }
308
+ else if (vnode) {
309
+ vNodeParents.push(vnode);
310
+ }
311
+ }
312
+ if (token.nesting === -1) {
313
+ vNodeParents.pop();
314
+ }
315
+ return isChild ? null : vnode;
316
+ })
317
+ .filter(node => !!node);
318
+ }
319
+ function render_(md) {
320
+ md.renderer.rules = { ...md.renderer.rules, ...defaultRules };
321
+ md.renderer.render = render;
322
+ md.renderer.renderInline = render;
323
+ md.renderer.renderAttrs = renderAttrs;
324
+ md.renderer.renderToken = renderToken;
325
+ }
326
+ export default render_;
@@ -0,0 +1,6 @@
1
+ import markdownit from 'markdown-it';
2
+ export declare const md: markdownit;
3
+ export declare const isKatexLoaded: import("vue").Ref<boolean, boolean>;
4
+ export declare const isShikiLoaded: import("vue").Ref<boolean, boolean>;
5
+ export declare function loadKatex(): Promise<void>;
6
+ export declare function loadShiki(): Promise<void>;
@@ -0,0 +1,65 @@
1
+ import markdownit from 'markdown-it';
2
+ import todo from 'markdown-it-todo';
3
+ import { ref } from 'vue';
4
+ import VNodePlugin from './render.js';
5
+ export const md = markdownit({
6
+ linkify: true,
7
+ typographer: true,
8
+ breaks: true,
9
+ html: true,
10
+ });
11
+ md.use(VNodePlugin);
12
+ md.use(todo);
13
+ // Lazy load katex and shiki to reduce initial bundle size
14
+ export const isKatexLoaded = ref(false);
15
+ export const isShikiLoaded = ref(false);
16
+ export async function loadKatex() {
17
+ if (isKatexLoaded.value) {
18
+ return;
19
+ }
20
+ const [katex, texmath] = await Promise.all([
21
+ import('katex'),
22
+ import('markdown-it-texmath'),
23
+ import('katex/dist/katex.min.css'), // Lazy load CSS
24
+ ]);
25
+ const tm = texmath.default.use(katex.default);
26
+ md.use(tm, { delimiters: ['brackets', 'dollars'] });
27
+ isKatexLoaded.value = true;
28
+ }
29
+ export async function loadShiki() {
30
+ if (isShikiLoaded.value) {
31
+ return;
32
+ }
33
+ try {
34
+ const Shiki = await import('@shikijs/markdown-it');
35
+ const shiki = await Shiki.default({
36
+ theme: 'vitesse-dark',
37
+ // Only load most commonly used languages to reduce bundle size
38
+ langs: [
39
+ 'javascript',
40
+ 'typescript',
41
+ 'python',
42
+ 'bash',
43
+ 'json',
44
+ 'html',
45
+ 'css',
46
+ 'markdown',
47
+ 'vue',
48
+ 'rust',
49
+ 'go',
50
+ 'java',
51
+ 'sql',
52
+ 'jsx',
53
+ 'tsx',
54
+ ],
55
+ fallbackLanguage: 'markdown',
56
+ });
57
+ md.use(shiki);
58
+ isShikiLoaded.value = true;
59
+ }
60
+ catch (error) {
61
+ console.error('Failed to load Shiki:', error);
62
+ // If Shiki fails to load, ensure repeated attempts are skipped.
63
+ isShikiLoaded.value = true;
64
+ }
65
+ }
@@ -0,0 +1,14 @@
1
+ import type { Ref, VNode } from 'vue';
2
+ export declare function createStreamingMarkdownVNodes(content: Readonly<Ref<string>>, loading: Readonly<Ref<boolean>>): {
3
+ contentFinal: import("vue").ComputedRef<string>;
4
+ contentVNodes: import("@vueuse/core").ComputedRefWithControl<VNode<import("vue").RendererNode, import("vue").RendererElement, {
5
+ [key: string]: any;
6
+ }>[]>;
7
+ debouncedLoading: Ref<boolean, boolean>;
8
+ formattedContent: import("vue").ComputedRef<string>;
9
+ };
10
+ export declare function createVNodeRendererComponent(vnodes: {
11
+ value: VNode[];
12
+ }): import("vue").DefineComponent<{}, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
13
+ [key: string]: any;
14
+ }>[], {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,62 @@
1
+ import { splitContent } from '@preferred-markdown-stream/core';
2
+ import { computedWithControl } from '@vueuse/core';
3
+ import { computed, defineComponent, ref, watch } from 'vue';
4
+ import { addFadeInToVNodes } from './fadeIn.js';
5
+ import { isKatexLoaded, isShikiLoaded, loadKatex, loadShiki, md, } from './runtime.js';
6
+ function ensureMarkdownCapabilities(content) {
7
+ if (content.includes('```') || content.includes('`')) {
8
+ loadShiki();
9
+ }
10
+ if (content.includes('$')
11
+ || content.includes(String.raw `\(`)
12
+ || content.includes(String.raw `\[`)) {
13
+ loadKatex();
14
+ }
15
+ }
16
+ function renderMarkdown(content) {
17
+ ensureMarkdownCapabilities(content);
18
+ return md.render(content, {
19
+ sanitize: true,
20
+ });
21
+ }
22
+ export function createStreamingMarkdownVNodes(content, loading) {
23
+ // Trailing-only debounce: true takes effect immediately,
24
+ // false is delayed so the last batch of content still animates.
25
+ const debouncedLoading = ref(loading.value);
26
+ let offTimer;
27
+ watch(loading, (val) => {
28
+ if (offTimer !== undefined) {
29
+ clearTimeout(offTimer);
30
+ offTimer = undefined;
31
+ }
32
+ if (val) {
33
+ debouncedLoading.value = true;
34
+ }
35
+ else {
36
+ offTimer = setTimeout(() => {
37
+ debouncedLoading.value = false;
38
+ }, 1000);
39
+ }
40
+ });
41
+ const formattedContent = computed(() => splitContent(content.value));
42
+ const contentFinal = computed(() => loading.value ? formattedContent.value : content.value);
43
+ const contentVNodes = computedWithControl(() => [isShikiLoaded.value, isKatexLoaded.value, contentFinal.value], () => {
44
+ const currentContent = contentFinal.value ?? '';
45
+ const rendered = renderMarkdown(currentContent);
46
+ return addFadeInToVNodes(rendered, debouncedLoading.value);
47
+ });
48
+ return {
49
+ contentFinal,
50
+ contentVNodes,
51
+ debouncedLoading,
52
+ formattedContent,
53
+ };
54
+ }
55
+ export function createVNodeRendererComponent(vnodes) {
56
+ return defineComponent({
57
+ setup() {
58
+ // eslint-disable-next-line unicorn/consistent-function-scoping
59
+ return () => vnodes.value;
60
+ },
61
+ });
62
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@preferred-markdown-stream/vue",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "private": false,
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "files": [
18
+ "README.md",
19
+ "dist"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p ./tsconfig.build.json",
26
+ "test": "vitest run --config ./vitest.config.ts",
27
+ "typecheck": "tsc -p ./tsconfig.json"
28
+ },
29
+ "peerDependencies": {
30
+ "vue": "^3.5.0"
31
+ },
32
+ "dependencies": {
33
+ "@preferred-markdown-stream/core": "workspace:*",
34
+ "@shikijs/markdown-it": "^4.0.2",
35
+ "@vueuse/core": "^14.2.1",
36
+ "katex": "^0.16.42",
37
+ "markdown-it": "^14.1.1",
38
+ "markdown-it-texmath": "^1.0.0",
39
+ "markdown-it-todo": "^1.0.5"
40
+ },
41
+ "devDependencies": {
42
+ "@types/markdown-it": "^14.1.2",
43
+ "typescript": "^6.0.2",
44
+ "vitest": "^4.1.1",
45
+ "vue": "^3.5.30"
46
+ }
47
+ }