@comark/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/dist/components/Comark.d.ts +77 -0
- package/dist/components/Comark.js +131 -0
- package/dist/components/ComarkRenderer.d.ts +57 -0
- package/dist/components/ComarkRenderer.js +288 -0
- package/dist/components/Math.d.ts +33 -0
- package/dist/components/Math.js +47 -0
- package/dist/components/Mermaid.d.ts +62 -0
- package/dist/components/Mermaid.js +101 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +164 -0
- package/dist/parse.d.ts +1 -0
- package/dist/parse.js +1 -0
- package/dist/plugins/alert.d.ts +2 -0
- package/dist/plugins/alert.js +2 -0
- package/dist/plugins/emoji.d.ts +2 -0
- package/dist/plugins/emoji.js +2 -0
- package/dist/plugins/headings.d.ts +2 -0
- package/dist/plugins/headings.js +2 -0
- package/dist/plugins/highlight.d.ts +2 -0
- package/dist/plugins/highlight.js +2 -0
- package/dist/plugins/math.d.ts +3 -0
- package/dist/plugins/math.js +3 -0
- package/dist/plugins/mermaid.d.ts +3 -0
- package/dist/plugins/mermaid.js +3 -0
- package/dist/plugins/security.d.ts +2 -0
- package/dist/plugins/security.js +2 -0
- package/dist/plugins/summary.d.ts +2 -0
- package/dist/plugins/summary.js +2 -0
- package/dist/plugins/task-list.d.ts +2 -0
- package/dist/plugins/task-list.js +2 -0
- package/dist/plugins/toc.d.ts +2 -0
- package/dist/plugins/toc.js +2 -0
- package/dist/render.d.ts +1 -0
- package/dist/render.js +1 -0
- package/dist/utils/caret.d.ts +7 -0
- package/dist/utils/caret.js +38 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/node.d.ts +8 -0
- package/dist/utils/node.js +83 -0
- package/dist/utils/slot.d.ts +3 -0
- package/dist/utils/slot.js +8 -0
- package/dist/utils/ssrSlot.d.ts +1 -0
- package/dist/utils/ssrSlot.js +8 -0
- package/dist/vite.d.ts +21 -0
- package/dist/vite.js +175 -0
- package/package.json +58 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineComponent } from 'vue';
|
|
2
|
+
import type { ParseOptions, ComponentManifest } from 'comark';
|
|
3
|
+
/**
|
|
4
|
+
* Props for the Comark component
|
|
5
|
+
*/
|
|
6
|
+
export interface ComarkProps {
|
|
7
|
+
/**
|
|
8
|
+
* The markdown content to parse and render
|
|
9
|
+
*/
|
|
10
|
+
markdown?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Parser options (excluding plugins)
|
|
13
|
+
*/
|
|
14
|
+
options?: Exclude<ParseOptions, 'plugins'>;
|
|
15
|
+
/**
|
|
16
|
+
* Additional plugins to use
|
|
17
|
+
*/
|
|
18
|
+
plugins?: ParseOptions['plugins'];
|
|
19
|
+
/**
|
|
20
|
+
* Custom component mappings for element tags
|
|
21
|
+
*/
|
|
22
|
+
components?: Record<string, any>;
|
|
23
|
+
/**
|
|
24
|
+
* Dynamic component resolver function
|
|
25
|
+
*/
|
|
26
|
+
componentsManifest?: ComponentManifest;
|
|
27
|
+
/**
|
|
28
|
+
* Enable streaming mode with stream-specific components
|
|
29
|
+
*/
|
|
30
|
+
streaming?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* If document has a <!-- more --> comment, only render the content before the comment
|
|
33
|
+
*/
|
|
34
|
+
summary?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* If caret is true, a caret will be appended to the last text node in the tree
|
|
37
|
+
*/
|
|
38
|
+
caret?: boolean | {
|
|
39
|
+
class: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
type ComarkComponent = ReturnType<typeof defineComponent<ComarkProps>>;
|
|
43
|
+
/**
|
|
44
|
+
* Comark component
|
|
45
|
+
*
|
|
46
|
+
* Comark component that accepts markdown as a string prop,
|
|
47
|
+
* parses it, and renders it.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```vue
|
|
51
|
+
* <template>
|
|
52
|
+
* <Comark :markdown="content" :components="customComponents" />
|
|
53
|
+
* </template>
|
|
54
|
+
*
|
|
55
|
+
* <script setup lang="ts">
|
|
56
|
+
* import { Comark } from '@comark/vue'
|
|
57
|
+
* import CustomHeading from './CustomHeading.vue'
|
|
58
|
+
*
|
|
59
|
+
* const content = `
|
|
60
|
+
* # Hello World
|
|
61
|
+
*
|
|
62
|
+
* This is a **markdown** document with components.
|
|
63
|
+
*
|
|
64
|
+
* ::alert{type="info"}
|
|
65
|
+
* This is an alert component
|
|
66
|
+
* ::
|
|
67
|
+
* `
|
|
68
|
+
*
|
|
69
|
+
* const customComponents = {
|
|
70
|
+
* h1: CustomHeading,
|
|
71
|
+
* alert: AlertComponent,
|
|
72
|
+
* }
|
|
73
|
+
* </script>
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare const Comark: ComarkComponent;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { computed, defineComponent, h, shallowRef, watch } from 'vue';
|
|
2
|
+
import { createParse } from 'comark';
|
|
3
|
+
import { ComarkRenderer } from "./ComarkRenderer.js";
|
|
4
|
+
/**
|
|
5
|
+
* Comark component
|
|
6
|
+
*
|
|
7
|
+
* Comark component that accepts markdown as a string prop,
|
|
8
|
+
* parses it, and renders it.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```vue
|
|
12
|
+
* <template>
|
|
13
|
+
* <Comark :markdown="content" :components="customComponents" />
|
|
14
|
+
* </template>
|
|
15
|
+
*
|
|
16
|
+
* <script setup lang="ts">
|
|
17
|
+
* import { Comark } from '@comark/vue'
|
|
18
|
+
* import CustomHeading from './CustomHeading.vue'
|
|
19
|
+
*
|
|
20
|
+
* const content = `
|
|
21
|
+
* # Hello World
|
|
22
|
+
*
|
|
23
|
+
* This is a **markdown** document with components.
|
|
24
|
+
*
|
|
25
|
+
* ::alert{type="info"}
|
|
26
|
+
* This is an alert component
|
|
27
|
+
* ::
|
|
28
|
+
* `
|
|
29
|
+
*
|
|
30
|
+
* const customComponents = {
|
|
31
|
+
* h1: CustomHeading,
|
|
32
|
+
* alert: AlertComponent,
|
|
33
|
+
* }
|
|
34
|
+
* </script>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const Comark = defineComponent({
|
|
38
|
+
name: 'Comark',
|
|
39
|
+
props: {
|
|
40
|
+
/**
|
|
41
|
+
* The markdown content to parse and render
|
|
42
|
+
*/
|
|
43
|
+
markdown: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: undefined,
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Parser options
|
|
49
|
+
*/
|
|
50
|
+
options: {
|
|
51
|
+
type: Object,
|
|
52
|
+
default: () => ({}),
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Additional plugins to use
|
|
56
|
+
*/
|
|
57
|
+
plugins: {
|
|
58
|
+
type: Array,
|
|
59
|
+
default: () => [],
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* Custom component mappings for element tags
|
|
63
|
+
* Key: tag name (e.g., 'h1', 'p', 'MyComponent')
|
|
64
|
+
* Value: Vue component
|
|
65
|
+
*/
|
|
66
|
+
components: {
|
|
67
|
+
type: Object,
|
|
68
|
+
default: () => ({}),
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* Dynamic component resolver function
|
|
72
|
+
* Used to resolve components that aren't in the components map
|
|
73
|
+
*/
|
|
74
|
+
componentsManifest: {
|
|
75
|
+
type: Function,
|
|
76
|
+
default: undefined,
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Enable streaming mode with stream-specific components
|
|
80
|
+
*/
|
|
81
|
+
streaming: {
|
|
82
|
+
type: Boolean,
|
|
83
|
+
default: false,
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* If document has a <!-- more --> comment, only render the content before the comment
|
|
87
|
+
*/
|
|
88
|
+
summary: {
|
|
89
|
+
type: Boolean,
|
|
90
|
+
default: false,
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* If caret is true, a caret will be appended to the last text node in the tree
|
|
94
|
+
*/
|
|
95
|
+
caret: {
|
|
96
|
+
type: [Boolean, Object],
|
|
97
|
+
default: false,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
async setup(props, ctx) {
|
|
101
|
+
const markdown = computed(() => {
|
|
102
|
+
let result = props.markdown;
|
|
103
|
+
const childrent = ctx.slots.default?.();
|
|
104
|
+
if (childrent && childrent.length > 0 && typeof childrent[0].children === 'string') {
|
|
105
|
+
result = childrent[0].children;
|
|
106
|
+
}
|
|
107
|
+
if (props.summary) {
|
|
108
|
+
result = result?.split('<!-- more -->')[0];
|
|
109
|
+
}
|
|
110
|
+
return (result || '').trim();
|
|
111
|
+
});
|
|
112
|
+
const parsed = shallowRef(null);
|
|
113
|
+
const parse = createParse({ ...props.options, plugins: props.plugins });
|
|
114
|
+
async function parseMarkdown() {
|
|
115
|
+
parsed.value = await parse(markdown.value, { streaming: props.streaming });
|
|
116
|
+
}
|
|
117
|
+
watch(markdown, parseMarkdown);
|
|
118
|
+
await parseMarkdown();
|
|
119
|
+
return () => {
|
|
120
|
+
// Render using ComarkRenderer
|
|
121
|
+
return h(ComarkRenderer, {
|
|
122
|
+
tree: parsed.value || { nodes: [], frontmatter: {}, meta: {} },
|
|
123
|
+
components: props.components,
|
|
124
|
+
streaming: props.streaming,
|
|
125
|
+
componentsManifest: props.componentsManifest,
|
|
126
|
+
class: `comark-content ${props.streaming ? 'comark-stream' : ''}`,
|
|
127
|
+
caret: props.caret,
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ComponentManifest, ComarkTree } from 'comark';
|
|
2
|
+
import { defineComponent } from 'vue';
|
|
3
|
+
/**
|
|
4
|
+
* Props for the ComarkRenderer component
|
|
5
|
+
*/
|
|
6
|
+
export interface ComarkRendererProps {
|
|
7
|
+
/**
|
|
8
|
+
* The Comark tree to render
|
|
9
|
+
*/
|
|
10
|
+
tree: ComarkTree;
|
|
11
|
+
/**
|
|
12
|
+
* Custom component mappings for element tags
|
|
13
|
+
*/
|
|
14
|
+
components?: Record<string, any>;
|
|
15
|
+
/**
|
|
16
|
+
* Dynamic component resolver function
|
|
17
|
+
*/
|
|
18
|
+
componentsManifest?: ComponentManifest;
|
|
19
|
+
/**
|
|
20
|
+
* Enable streaming mode with enhanced components
|
|
21
|
+
*/
|
|
22
|
+
streaming?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* If caret is true, a caret will be appended to the last text node in the tree
|
|
25
|
+
*/
|
|
26
|
+
caret?: boolean | {
|
|
27
|
+
class: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
type ComarkRendererComponent = ReturnType<typeof defineComponent<ComarkRendererProps>>;
|
|
31
|
+
/**
|
|
32
|
+
* ComarkRenderer component
|
|
33
|
+
*
|
|
34
|
+
* Renders a Comark tree to Vue components/HTML.
|
|
35
|
+
* Supports custom component mapping for element tags.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```vue
|
|
39
|
+
* <template>
|
|
40
|
+
* <ComarkTree :tree="comarktree" :components="customComponents" />
|
|
41
|
+
* </template>
|
|
42
|
+
*
|
|
43
|
+
* <script setup lang="ts">
|
|
44
|
+
* import { ComarkRenderer } from '@comark/vue'
|
|
45
|
+
* import CustomHeading from './CustomHeading.vue'
|
|
46
|
+
*
|
|
47
|
+
* const customComponents = {
|
|
48
|
+
* h1: CustomHeading,
|
|
49
|
+
* h2: CustomHeading,
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* const comarktree = await parse(`This is **markdown** with components.`)
|
|
53
|
+
* </script>
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare const ComarkRenderer: ComarkRendererComponent;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { computed, defineAsyncComponent, defineComponent, getCurrentInstance, h, inject, onErrorCaptured, ref, toRaw } from 'vue';
|
|
2
|
+
import { findLastTextNodeAndAppendNode, getCaret } from "../utils/caret.js";
|
|
3
|
+
// Cache for dynamically resolved components
|
|
4
|
+
const asyncComponentCache = new Map();
|
|
5
|
+
const camelize = (s) => s.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '');
|
|
6
|
+
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
7
|
+
const pascalCase = (str) => capitalize(camelize(str));
|
|
8
|
+
/**
|
|
9
|
+
* Helper to get tag from a ComarkNode
|
|
10
|
+
*/
|
|
11
|
+
function getTag(node) {
|
|
12
|
+
if (Array.isArray(node) && node.length >= 1) {
|
|
13
|
+
return node[0];
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Helper to get props from a ComarkNode
|
|
19
|
+
*/
|
|
20
|
+
function getProps(node) {
|
|
21
|
+
if (Array.isArray(node) && node.length >= 2) {
|
|
22
|
+
return node[1] || {};
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
function parsePropValue(value) {
|
|
27
|
+
if (value === 'true')
|
|
28
|
+
return true;
|
|
29
|
+
if (value === 'false')
|
|
30
|
+
return false;
|
|
31
|
+
if (value === 'null')
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(value);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// noop
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Helper to get children from a ComarkNode
|
|
43
|
+
*/
|
|
44
|
+
function getChildren(node) {
|
|
45
|
+
if (Array.isArray(node) && node.length > 2) {
|
|
46
|
+
return node.slice(2);
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
function resolveComponent(tag, components, componentsManifest) {
|
|
51
|
+
const appComponents = getCurrentInstance()?.appContext?.components;
|
|
52
|
+
const pascalTag = pascalCase(tag);
|
|
53
|
+
const proseTag = `Prose${pascalTag}`;
|
|
54
|
+
let resolvedComponent = components[proseTag]
|
|
55
|
+
|| components[tag]
|
|
56
|
+
|| components[pascalTag]
|
|
57
|
+
// If the component is not found in the components map, try to find it in the app context
|
|
58
|
+
|| appComponents?.[proseTag]
|
|
59
|
+
|| appComponents?.[pascalTag];
|
|
60
|
+
// If not in components map and manifest is provided, try dynamic resolution
|
|
61
|
+
if (!resolvedComponent && componentsManifest) {
|
|
62
|
+
// Check cache first to avoid creating duplicate async components
|
|
63
|
+
const cacheKey = tag;
|
|
64
|
+
if (!asyncComponentCache.has(cacheKey)) {
|
|
65
|
+
const promise = componentsManifest(tag);
|
|
66
|
+
if (promise) {
|
|
67
|
+
asyncComponentCache.set(cacheKey, defineAsyncComponent(() => promise));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
resolvedComponent = asyncComponentCache.get(cacheKey);
|
|
71
|
+
}
|
|
72
|
+
return resolvedComponent;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Render a single Comark node to Vue VNode
|
|
76
|
+
*/
|
|
77
|
+
function renderNode(node, components = {}, key, componentsManifest, parent) {
|
|
78
|
+
// Handle text nodes (strings)
|
|
79
|
+
if (typeof node === 'string') {
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
// Handle element nodes (arrays)
|
|
83
|
+
if (Array.isArray(node)) {
|
|
84
|
+
const tag = getTag(node);
|
|
85
|
+
if (!tag)
|
|
86
|
+
return null;
|
|
87
|
+
const nodeProps = getProps(node);
|
|
88
|
+
const children = getChildren(node);
|
|
89
|
+
// Check if there's a custom component for this tag
|
|
90
|
+
let customComponent;
|
|
91
|
+
if (parent?.[0] !== 'pre') {
|
|
92
|
+
if (nodeProps.as) {
|
|
93
|
+
customComponent = resolveComponent(nodeProps.as, components, componentsManifest);
|
|
94
|
+
}
|
|
95
|
+
if (!customComponent) {
|
|
96
|
+
customComponent = resolveComponent(tag, components, componentsManifest);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const component = customComponent || tag;
|
|
100
|
+
// Prepare props
|
|
101
|
+
// Prepare props — use for...in instead of Object.entries() to avoid intermediate array allocation
|
|
102
|
+
const props = {};
|
|
103
|
+
for (const k in nodeProps) {
|
|
104
|
+
if (k === '$') {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (k === 'className') {
|
|
108
|
+
props.class = nodeProps[k];
|
|
109
|
+
}
|
|
110
|
+
else if (k.charCodeAt(0) === 58 /* ':' */) {
|
|
111
|
+
props[k.substring(1)] = parsePropValue(nodeProps[k]);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
props[k] = nodeProps[k];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// @ts-expect-error - component might be a Vue component
|
|
118
|
+
if (component?.props?.__node || component?.__asyncResolved?.props?.__node) {
|
|
119
|
+
props.__node = node;
|
|
120
|
+
}
|
|
121
|
+
// Add key if provided
|
|
122
|
+
if (key !== undefined) {
|
|
123
|
+
props.key = key;
|
|
124
|
+
}
|
|
125
|
+
if (node.length === 2) {
|
|
126
|
+
return h(component, props);
|
|
127
|
+
}
|
|
128
|
+
// Separate template elements (slots) from regular children
|
|
129
|
+
const slots = {};
|
|
130
|
+
const regularChildren = [];
|
|
131
|
+
for (let i = 0; i < children.length; i++) {
|
|
132
|
+
const child = children[i];
|
|
133
|
+
if (child === undefined || child === null)
|
|
134
|
+
continue;
|
|
135
|
+
// Check if this is a slot template (array with tag 'template')
|
|
136
|
+
const childTag = getTag(child);
|
|
137
|
+
const childProps = getProps(child);
|
|
138
|
+
if (childTag === 'template' && childProps) {
|
|
139
|
+
// Find the slot name from props
|
|
140
|
+
// Support both { name: 'title' } and { '#title': '' } formats
|
|
141
|
+
let slotName;
|
|
142
|
+
if (childProps.name) {
|
|
143
|
+
slotName = childProps.name;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Use for...in instead of Object.keys().find() — avoids intermediate array
|
|
147
|
+
for (const pk in childProps) {
|
|
148
|
+
if (pk.startsWith('v-slot:')) {
|
|
149
|
+
slotName = pk.substring(7);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (slotName) {
|
|
155
|
+
const slotChildren = getChildren(child);
|
|
156
|
+
slots[slotName] = () => slotChildren
|
|
157
|
+
.map((slotChild, idx) => renderNode(slotChild, components, idx, componentsManifest, node))
|
|
158
|
+
.filter((slotChild) => slotChild !== null);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const rendered = renderNode(child, components, i, componentsManifest, node);
|
|
163
|
+
if (rendered !== null) {
|
|
164
|
+
regularChildren.push(rendered);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// If using a custom component, pass slots
|
|
168
|
+
if (customComponent) {
|
|
169
|
+
// Always include default slot if there are regular children
|
|
170
|
+
if (regularChildren.length > 0) {
|
|
171
|
+
slots.default = () => regularChildren;
|
|
172
|
+
}
|
|
173
|
+
return h(component, props, slots);
|
|
174
|
+
}
|
|
175
|
+
// For native HTML tags, pass children directly (ignore slot templates)
|
|
176
|
+
return h(component, props, regularChildren);
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* ComarkRenderer component
|
|
182
|
+
*
|
|
183
|
+
* Renders a Comark tree to Vue components/HTML.
|
|
184
|
+
* Supports custom component mapping for element tags.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```vue
|
|
188
|
+
* <template>
|
|
189
|
+
* <ComarkTree :tree="comarktree" :components="customComponents" />
|
|
190
|
+
* </template>
|
|
191
|
+
*
|
|
192
|
+
* <script setup lang="ts">
|
|
193
|
+
* import { ComarkRenderer } from '@comark/vue'
|
|
194
|
+
* import CustomHeading from './CustomHeading.vue'
|
|
195
|
+
*
|
|
196
|
+
* const customComponents = {
|
|
197
|
+
* h1: CustomHeading,
|
|
198
|
+
* h2: CustomHeading,
|
|
199
|
+
* }
|
|
200
|
+
*
|
|
201
|
+
* const comarktree = await parse(`This is **markdown** with components.`)
|
|
202
|
+
* </script>
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export const ComarkRenderer = defineComponent({
|
|
206
|
+
name: 'ComarkRenderer',
|
|
207
|
+
props: {
|
|
208
|
+
/**
|
|
209
|
+
* The Comark tree to render
|
|
210
|
+
*/
|
|
211
|
+
tree: {
|
|
212
|
+
type: Object,
|
|
213
|
+
required: true,
|
|
214
|
+
},
|
|
215
|
+
/**
|
|
216
|
+
* Custom component mappings for element tags
|
|
217
|
+
* Key: tag name (e.g., 'h1', 'p', 'MyComponent')
|
|
218
|
+
* Value: Vue component
|
|
219
|
+
*/
|
|
220
|
+
components: {
|
|
221
|
+
type: Object,
|
|
222
|
+
default: () => ({}),
|
|
223
|
+
},
|
|
224
|
+
/**
|
|
225
|
+
* Dynamic component resolver function
|
|
226
|
+
* Used to resolve components that aren't in the components map
|
|
227
|
+
*/
|
|
228
|
+
componentsManifest: {
|
|
229
|
+
type: Function,
|
|
230
|
+
default: undefined,
|
|
231
|
+
},
|
|
232
|
+
/**
|
|
233
|
+
* Enable streaming mode with stream-specific components
|
|
234
|
+
*/
|
|
235
|
+
streaming: {
|
|
236
|
+
type: Boolean,
|
|
237
|
+
default: false,
|
|
238
|
+
},
|
|
239
|
+
/**
|
|
240
|
+
* If caret is true, a caret will be appended to the last text node in the tree
|
|
241
|
+
* If caret is an object, it will be appended to the last text node in the tree with the given class
|
|
242
|
+
*/
|
|
243
|
+
caret: {
|
|
244
|
+
type: [Boolean, Object],
|
|
245
|
+
default: false,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
async setup(props) {
|
|
249
|
+
const componentErrors = ref(new Set());
|
|
250
|
+
// Capture errors from child components (e.g., during streaming when props are incomplete)
|
|
251
|
+
onErrorCaptured((_err, instance, _info) => {
|
|
252
|
+
// Get component name from instance
|
|
253
|
+
const componentName = instance?.$?.type?.name || instance?.type?.name || 'unknown';
|
|
254
|
+
// Track failed component to prevent re-rendering during streaming
|
|
255
|
+
componentErrors.value.add(componentName);
|
|
256
|
+
// Prevent error from propagating (don't crash the app during streaming)
|
|
257
|
+
return false;
|
|
258
|
+
});
|
|
259
|
+
const comark = inject('comark', { components: {}, componentManifest: () => null });
|
|
260
|
+
const components = computed(() => ({
|
|
261
|
+
...comark?.components,
|
|
262
|
+
...props.components,
|
|
263
|
+
}));
|
|
264
|
+
const componentManifest = (name) => {
|
|
265
|
+
let resolved = props.componentsManifest?.(name);
|
|
266
|
+
if (!resolved) {
|
|
267
|
+
resolved = comark?.componentManifest(name);
|
|
268
|
+
}
|
|
269
|
+
return resolved || null;
|
|
270
|
+
};
|
|
271
|
+
const caret = computed(() => getCaret(props.caret || false));
|
|
272
|
+
return () => {
|
|
273
|
+
// Render all nodes from the tree value
|
|
274
|
+
const nodes = toRaw(props.tree.nodes || []) || [];
|
|
275
|
+
if (props.streaming && caret.value && nodes.length > 0) {
|
|
276
|
+
const hasStreamCaret = findLastTextNodeAndAppendNode(nodes[nodes.length - 1], caret.value);
|
|
277
|
+
if (!hasStreamCaret) {
|
|
278
|
+
nodes.push(caret.value);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const children = nodes
|
|
282
|
+
.map((node, index) => renderNode(node, components.value, index, componentManifest))
|
|
283
|
+
.filter((child) => child !== null);
|
|
284
|
+
// Wrap in a fragment
|
|
285
|
+
return h('div', { class: 'comark-content' }, children);
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import 'katex/dist/katex.min.css';
|
|
2
|
+
export declare const Math: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
3
|
+
content: {
|
|
4
|
+
type: StringConstructor;
|
|
5
|
+
required: true;
|
|
6
|
+
};
|
|
7
|
+
class: {
|
|
8
|
+
type: StringConstructor;
|
|
9
|
+
default: string;
|
|
10
|
+
};
|
|
11
|
+
__node: {
|
|
12
|
+
type: ObjectConstructor;
|
|
13
|
+
default: () => {};
|
|
14
|
+
};
|
|
15
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
18
|
+
content: {
|
|
19
|
+
type: StringConstructor;
|
|
20
|
+
required: true;
|
|
21
|
+
};
|
|
22
|
+
class: {
|
|
23
|
+
type: StringConstructor;
|
|
24
|
+
default: string;
|
|
25
|
+
};
|
|
26
|
+
__node: {
|
|
27
|
+
type: ObjectConstructor;
|
|
28
|
+
default: () => {};
|
|
29
|
+
};
|
|
30
|
+
}>> & Readonly<{}>, {
|
|
31
|
+
class: string;
|
|
32
|
+
__node: Record<string, any>;
|
|
33
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineComponent, h, computed, watch, ref } from 'vue';
|
|
2
|
+
import katex from 'katex';
|
|
3
|
+
import 'katex/dist/katex.min.css';
|
|
4
|
+
export const Math = defineComponent({
|
|
5
|
+
name: 'Math',
|
|
6
|
+
props: {
|
|
7
|
+
content: {
|
|
8
|
+
type: String,
|
|
9
|
+
required: true,
|
|
10
|
+
},
|
|
11
|
+
class: {
|
|
12
|
+
type: String,
|
|
13
|
+
default: '',
|
|
14
|
+
},
|
|
15
|
+
__node: {
|
|
16
|
+
type: Object,
|
|
17
|
+
default: () => ({}),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
setup(props) {
|
|
21
|
+
const isInline = computed(() => {
|
|
22
|
+
return props.class?.includes('inline');
|
|
23
|
+
});
|
|
24
|
+
const mathml = ref('...');
|
|
25
|
+
watch(() => props.content, () => {
|
|
26
|
+
try {
|
|
27
|
+
mathml.value = katex.renderToString(props.content, {
|
|
28
|
+
throwOnError: true,
|
|
29
|
+
displayMode: !isInline.value,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Keep loading state on error
|
|
34
|
+
}
|
|
35
|
+
}, { immediate: true });
|
|
36
|
+
if (isInline.value) {
|
|
37
|
+
return () => h('span', {
|
|
38
|
+
class: 'math inline',
|
|
39
|
+
innerHTML: mathml.value,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return () => h('div', {
|
|
43
|
+
class: 'math block',
|
|
44
|
+
innerHTML: mathml.value,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PropType } from 'vue';
|
|
2
|
+
import { type DiagramColors } from 'beautiful-mermaid';
|
|
3
|
+
import type { ThemeNames } from 'comark/plugins/mermaid';
|
|
4
|
+
export declare const Mermaid: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
5
|
+
content: {
|
|
6
|
+
type: StringConstructor;
|
|
7
|
+
required: true;
|
|
8
|
+
};
|
|
9
|
+
class: {
|
|
10
|
+
type: StringConstructor;
|
|
11
|
+
default: string;
|
|
12
|
+
};
|
|
13
|
+
height: {
|
|
14
|
+
type: StringConstructor;
|
|
15
|
+
default: string;
|
|
16
|
+
};
|
|
17
|
+
width: {
|
|
18
|
+
type: StringConstructor;
|
|
19
|
+
default: string;
|
|
20
|
+
};
|
|
21
|
+
theme: {
|
|
22
|
+
type: PropType<ThemeNames | DiagramColors>;
|
|
23
|
+
default: undefined;
|
|
24
|
+
};
|
|
25
|
+
themeDark: {
|
|
26
|
+
type: PropType<ThemeNames | DiagramColors>;
|
|
27
|
+
default: undefined;
|
|
28
|
+
};
|
|
29
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
32
|
+
content: {
|
|
33
|
+
type: StringConstructor;
|
|
34
|
+
required: true;
|
|
35
|
+
};
|
|
36
|
+
class: {
|
|
37
|
+
type: StringConstructor;
|
|
38
|
+
default: string;
|
|
39
|
+
};
|
|
40
|
+
height: {
|
|
41
|
+
type: StringConstructor;
|
|
42
|
+
default: string;
|
|
43
|
+
};
|
|
44
|
+
width: {
|
|
45
|
+
type: StringConstructor;
|
|
46
|
+
default: string;
|
|
47
|
+
};
|
|
48
|
+
theme: {
|
|
49
|
+
type: PropType<ThemeNames | DiagramColors>;
|
|
50
|
+
default: undefined;
|
|
51
|
+
};
|
|
52
|
+
themeDark: {
|
|
53
|
+
type: PropType<ThemeNames | DiagramColors>;
|
|
54
|
+
default: undefined;
|
|
55
|
+
};
|
|
56
|
+
}>> & Readonly<{}>, {
|
|
57
|
+
class: string;
|
|
58
|
+
height: string;
|
|
59
|
+
width: string;
|
|
60
|
+
theme: ThemeNames | DiagramColors;
|
|
61
|
+
themeDark: ThemeNames | DiagramColors;
|
|
62
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|