@comark/vue 0.3.1 β 0.4.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 +104 -0
- package/dist/components/Comark.js +2 -3
- package/dist/components/ComarkRenderer.js +18 -9
- package/dist/components/Math.d.ts +0 -1
- package/dist/components/Math.js +0 -1
- package/dist/components/Mermaid.js +3 -3
- package/dist/index.js +1 -4
- package/dist/plugins/syntax.d.ts +2 -0
- package/dist/plugins/syntax.js +2 -0
- package/dist/utils/node.js +10 -8
- package/dist/vite.js +16 -11
- package/package.json +32 -23
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<img src="https://github.com/comarkdown/comark/blob/main/assets/banner.jpg" width="100%" alt="Comark banner" />
|
|
2
|
+
|
|
3
|
+
# @comark/vue
|
|
4
|
+
|
|
5
|
+
[](https://npmx.dev/@comark/vue)
|
|
6
|
+
[](https://npm.chart.dev/@comark/vue)
|
|
7
|
+
[](https://github.com/comarkdown/comark/actions/workflows/ci.yml)
|
|
8
|
+
[](https://comark.dev/rendering/vue)
|
|
9
|
+
[](https://github.com/comarkdown/comark/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
Vue renderer for [Comark](https://comark.dev) β render markdown with custom Vue components, streaming support, and SSR.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- π§© `<Comark>` component for one-shot markdown rendering
|
|
16
|
+
- π― Map any Comark tag to a custom Vue component
|
|
17
|
+
- π Streaming-friendly with auto-close and caret support
|
|
18
|
+
- π₯οΈ SSR-safe, works in Nuxt and Vite
|
|
19
|
+
- π¨ Optional Vite plugin for compile-time component resolution
|
|
20
|
+
- π Plugin ecosystem (math, mermaid, highlight, bindingβ¦)
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @comark/vue
|
|
26
|
+
# or
|
|
27
|
+
pnpm add @comark/vue
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```vue
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { Comark } from '@comark/vue'
|
|
35
|
+
import math, { Math } from '@comark/vue/plugins/math'
|
|
36
|
+
|
|
37
|
+
const content = `# Hello\n\nThis is **Comark** in Vue.`
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<Comark :components="{ math: Math }" :plugins="[math()]">
|
|
42
|
+
{{ content }}
|
|
43
|
+
</Comark>
|
|
44
|
+
</template>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Custom components
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import { Comark } from '@comark/vue'
|
|
52
|
+
import Alert from './components/Alert.vue'
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<Comark :components="{ alert: Alert }">{{ content }}</Comark>
|
|
57
|
+
</template>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```mdc
|
|
61
|
+
::alert{type="warning"}
|
|
62
|
+
Heads up!
|
|
63
|
+
::
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Streaming
|
|
67
|
+
|
|
68
|
+
```vue
|
|
69
|
+
<Comark :streaming="isStreaming" caret>{{ content }}</Comark>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Using with Vite
|
|
73
|
+
|
|
74
|
+
`@comark/vue` ships an optional Vite plugin that:
|
|
75
|
+
|
|
76
|
+
- Enables `<slot unwrap="...">` inside custom markdown components.
|
|
77
|
+
- Auto-registers every `.vue` file under `src/components/prose` (or `components/prose`) as a global component.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// vite.config.ts
|
|
81
|
+
import { defineConfig } from 'vite'
|
|
82
|
+
import vue from '@vitejs/plugin-vue'
|
|
83
|
+
import comark from '@comark/vue/vite'
|
|
84
|
+
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
plugins: [vue(), comark()],
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Pass `comark({ prose: false })` to opt out of the auto-registered prose components.
|
|
91
|
+
|
|
92
|
+
## Using with Nuxt
|
|
93
|
+
|
|
94
|
+
For Nuxt, use [`@comark/nuxt`](https://comark.dev/rendering/nuxt) which auto-imports the `<Comark>` component and wires up `~/components/prose` overrides.
|
|
95
|
+
|
|
96
|
+
## Documentation
|
|
97
|
+
|
|
98
|
+
Full guide and API reference at [comark.dev/rendering/vue](https://comark.dev/rendering/vue).
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
Made with β€οΈ
|
|
103
|
+
|
|
104
|
+
Published under [MIT License](https://github.com/comarkdown/comark/blob/main/LICENSE).
|
|
@@ -118,9 +118,8 @@ export const Comark = defineComponent({
|
|
|
118
118
|
});
|
|
119
119
|
const parsed = shallowRef(null);
|
|
120
120
|
const parse = createSerializedParse({ ...props.options, plugins: props.plugins });
|
|
121
|
-
watch(() => [markdown.value, props.streaming], () => parse(markdown.value, { streaming: props.streaming }).then(result => parsed.value = result));
|
|
122
|
-
await parse(markdown.value, { streaming: props.streaming })
|
|
123
|
-
.then(result => parsed.value = result);
|
|
121
|
+
watch(() => [markdown.value, props.streaming], () => parse(markdown.value, { streaming: props.streaming }).then((result) => (parsed.value = result)));
|
|
122
|
+
await parse(markdown.value, { streaming: props.streaming }).then((result) => (parsed.value = result));
|
|
124
123
|
return () => {
|
|
125
124
|
// Render using ComarkRenderer
|
|
126
125
|
return h(ComarkRenderer, {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { computed, defineAsyncComponent, defineComponent, getCurrentInstance, h, inject, onErrorCaptured, ref, toRaw } from 'vue';
|
|
1
|
+
import { computed, defineAsyncComponent, defineComponent, getCurrentInstance, h, inject, onErrorCaptured, ref, toRaw, } from 'vue';
|
|
2
2
|
import { findLastTextNodeAndAppendNode, getCaret } from "../utils/caret.js";
|
|
3
3
|
import { pascalCase, resolveAttributes } from 'comark/utils';
|
|
4
4
|
// Cache for dynamically resolved components
|
|
5
5
|
const asyncComponentCache = new Map();
|
|
6
|
+
function isPromiseLike(value) {
|
|
7
|
+
return !!value && typeof value.then === 'function';
|
|
8
|
+
}
|
|
9
|
+
function unwrapComponent(mod) {
|
|
10
|
+
return mod && typeof mod === 'object' && 'default' in mod ? mod.default : mod;
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
13
|
* Helper to get tag from a ComarkNode
|
|
8
14
|
*/
|
|
@@ -34,20 +40,23 @@ function resolveComponent(tag, components, componentsManifest) {
|
|
|
34
40
|
const appComponents = getCurrentInstance()?.appContext?.components;
|
|
35
41
|
const pascalTag = pascalCase(tag);
|
|
36
42
|
const proseTag = `Prose${pascalTag}`;
|
|
37
|
-
let resolvedComponent = components[proseTag]
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
let resolvedComponent = components[proseTag] ||
|
|
44
|
+
components[tag] ||
|
|
45
|
+
components[pascalTag] ||
|
|
40
46
|
// If the component is not found in the components map, try to find it in the app context
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
appComponents?.[proseTag] ||
|
|
48
|
+
appComponents?.[pascalTag];
|
|
43
49
|
// If not in components map and manifest is provided, try dynamic resolution
|
|
44
50
|
if (!resolvedComponent && componentsManifest) {
|
|
45
51
|
// Check cache first to avoid creating duplicate async components
|
|
46
52
|
const cacheKey = tag;
|
|
47
53
|
if (!asyncComponentCache.has(cacheKey)) {
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
asyncComponentCache.set(cacheKey, defineAsyncComponent(() =>
|
|
54
|
+
const resolved = componentsManifest(tag);
|
|
55
|
+
if (isPromiseLike(resolved)) {
|
|
56
|
+
asyncComponentCache.set(cacheKey, defineAsyncComponent(() => resolved));
|
|
57
|
+
}
|
|
58
|
+
else if (resolved) {
|
|
59
|
+
asyncComponentCache.set(cacheKey, unwrapComponent(resolved));
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
resolvedComponent = asyncComponentCache.get(cacheKey);
|
package/dist/components/Math.js
CHANGED
|
@@ -86,15 +86,15 @@ export const Mermaid = defineComponent({
|
|
|
86
86
|
});
|
|
87
87
|
return () => {
|
|
88
88
|
return h('div', {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
class: `mermaid ${props.class}`,
|
|
90
|
+
style: {
|
|
91
91
|
display: 'flex',
|
|
92
92
|
justifyContent: 'center',
|
|
93
93
|
width: props.width,
|
|
94
94
|
height: props.height,
|
|
95
95
|
},
|
|
96
96
|
'data-error': error.value,
|
|
97
|
-
|
|
97
|
+
innerHTML: svgContent.value,
|
|
98
98
|
});
|
|
99
99
|
};
|
|
100
100
|
},
|
package/dist/index.js
CHANGED
|
@@ -73,10 +73,7 @@ export function defineComarkComponent(config = {}) {
|
|
|
73
73
|
...parseOptions,
|
|
74
74
|
...props.options,
|
|
75
75
|
}));
|
|
76
|
-
const plugins = computed(() => [
|
|
77
|
-
...(config.plugins || []),
|
|
78
|
-
...(props.plugins || []),
|
|
79
|
-
]);
|
|
76
|
+
const plugins = computed(() => [...(config.plugins || []), ...(props.plugins || [])]);
|
|
80
77
|
const components = computed(() => ({
|
|
81
78
|
...config.components,
|
|
82
79
|
...props.components,
|
package/dist/utils/node.js
CHANGED
|
@@ -38,12 +38,12 @@ export function nodeTextContent(node) {
|
|
|
38
38
|
}
|
|
39
39
|
export function unwrap(vnode, tags = []) {
|
|
40
40
|
if (Array.isArray(vnode)) {
|
|
41
|
-
return vnode.flatMap(node => unwrap(node, tags));
|
|
41
|
+
return vnode.flatMap((node) => unwrap(node, tags));
|
|
42
42
|
}
|
|
43
43
|
let result = vnode;
|
|
44
|
-
if (tags.some(tag => tag === '*' || isTag(vnode, tag))) {
|
|
44
|
+
if (tags.some((tag) => tag === '*' || isTag(vnode, tag))) {
|
|
45
45
|
result = nodeChildren(vnode) || vnode;
|
|
46
|
-
if (!Array.isArray(result) && TEXT_TAGS.some(tag => isTag(vnode, tag))) {
|
|
46
|
+
if (!Array.isArray(result) && TEXT_TAGS.some((tag) => isTag(vnode, tag))) {
|
|
47
47
|
result = [result];
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -55,18 +55,20 @@ function _flatUnwrap(vnodes, tags = []) {
|
|
|
55
55
|
return vnodes;
|
|
56
56
|
}
|
|
57
57
|
return vnodes
|
|
58
|
-
.flatMap(vnode => _flatUnwrap(unwrap(vnode, [tags[0]]), tags.slice(1)))
|
|
59
|
-
.filter(vnode => !(isText(vnode) && nodeTextContent(vnode).trim() === ''));
|
|
58
|
+
.flatMap((vnode) => _flatUnwrap(unwrap(vnode, [tags[0]]), tags.slice(1)))
|
|
59
|
+
.filter((vnode) => !(isText(vnode) && nodeTextContent(vnode).trim() === ''));
|
|
60
60
|
}
|
|
61
61
|
export function flatUnwrap(vnodes, tags = []) {
|
|
62
62
|
if (typeof tags === 'string') {
|
|
63
|
-
tags = tags
|
|
63
|
+
tags = tags
|
|
64
|
+
.split(/[,\s]/)
|
|
65
|
+
.map((tag) => tag.trim())
|
|
66
|
+
.filter(Boolean);
|
|
64
67
|
}
|
|
65
68
|
if (!tags.length) {
|
|
66
69
|
return vnodes;
|
|
67
70
|
}
|
|
68
|
-
return _flatUnwrap(vnodes, tags)
|
|
69
|
-
.reduce((acc, item) => {
|
|
71
|
+
return _flatUnwrap(vnodes, tags).reduce((acc, item) => {
|
|
70
72
|
if (isText(item)) {
|
|
71
73
|
if (typeof acc[acc.length - 1] === 'string') {
|
|
72
74
|
acc[acc.length - 1] += item.children;
|
package/dist/vite.js
CHANGED
|
@@ -3,13 +3,18 @@ import { readdir } from 'node:fs/promises';
|
|
|
3
3
|
import { join, basename } from 'node:path';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
const runtimeDir = fileURLToPath(new URL('./utils', import.meta.url));
|
|
6
|
+
function toImportPath(filePath) {
|
|
7
|
+
return filePath.replace(/\\/g, '/');
|
|
8
|
+
}
|
|
6
9
|
function viteComarkSlot(node, context) {
|
|
7
|
-
const isVueSlotWithUnwrap = node.tag === 'slot'
|
|
8
|
-
|
|
10
|
+
const isVueSlotWithUnwrap = node.tag === 'slot' &&
|
|
11
|
+
node.props.find((p) => p.name === 'unwrap' ||
|
|
12
|
+
p.name === 'mdc-unwrap' ||
|
|
13
|
+
(p.name === 'bind' && p.rawName === ':comark-unwrap'));
|
|
9
14
|
if (isVueSlotWithUnwrap) {
|
|
10
15
|
const transform = context.ssr
|
|
11
|
-
? context.nodeTransforms.find(nt => nt.name === 'ssrTransformSlotOutlet')
|
|
12
|
-
: context.nodeTransforms.find(nt => nt.name === 'transformSlotOutlet');
|
|
16
|
+
? context.nodeTransforms.find((nt) => nt.name === 'ssrTransformSlotOutlet')
|
|
17
|
+
: context.nodeTransforms.find((nt) => nt.name === 'transformSlotOutlet');
|
|
13
18
|
return () => {
|
|
14
19
|
node.tag = 'slot';
|
|
15
20
|
node.type = 1;
|
|
@@ -20,16 +25,16 @@ function viteComarkSlot(node, context) {
|
|
|
20
25
|
const importExp = context.ssr
|
|
21
26
|
? '{ ssrRenderSlot as _ssrRenderComarkSlot }'
|
|
22
27
|
: '{ renderSlot as _renderComarkSlot }';
|
|
23
|
-
if (!context.imports.some(i => String(i.exp) === importExp)) {
|
|
28
|
+
if (!context.imports.some((i) => String(i.exp) === importExp)) {
|
|
24
29
|
context.imports.push({
|
|
25
30
|
exp: importExp,
|
|
26
|
-
path: `${runtimeDir}/${context.ssr ? 'ssrSlot' : 'slot'}`,
|
|
31
|
+
path: `${toImportPath(runtimeDir)}/${context.ssr ? 'ssrSlot' : 'slot'}`,
|
|
27
32
|
});
|
|
28
33
|
}
|
|
29
34
|
};
|
|
30
35
|
}
|
|
31
36
|
if (context.nodeTransforms[0]?.name !== 'viteComarkSlot') {
|
|
32
|
-
const index = context.nodeTransforms.findIndex(f => f.name === 'viteComarkSlot');
|
|
37
|
+
const index = context.nodeTransforms.findIndex((f) => f.name === 'viteComarkSlot');
|
|
33
38
|
if (index !== -1) {
|
|
34
39
|
const nt = context.nodeTransforms.splice(index, 1);
|
|
35
40
|
context.nodeTransforms.unshift(nt[0]);
|
|
@@ -64,7 +69,7 @@ function generateComponentsModule(files) {
|
|
|
64
69
|
}
|
|
65
70
|
const lines = [];
|
|
66
71
|
for (let i = 0; i < files.length; i++) {
|
|
67
|
-
lines.push(`import __comp_${i} from ${JSON.stringify(files[i]
|
|
72
|
+
lines.push(`import __comp_${i} from ${JSON.stringify(toImportPath(files[i]))}`);
|
|
68
73
|
}
|
|
69
74
|
lines.push('');
|
|
70
75
|
lines.push('export default {');
|
|
@@ -122,7 +127,7 @@ export default function comark(opts = {}) {
|
|
|
122
127
|
if (existsSync(join(config.root, 'components', 'prose'))) {
|
|
123
128
|
proseDir = join(config.root, 'components', 'prose');
|
|
124
129
|
}
|
|
125
|
-
const vuePlugin = config.plugins.find(p => p.name === 'vite:vue');
|
|
130
|
+
const vuePlugin = config.plugins.find((p) => p.name === 'vite:vue');
|
|
126
131
|
if (!vuePlugin) {
|
|
127
132
|
console.warn('[comark-vue] @vitejs/plugin-vue not found. Make sure vue() is in your plugins.');
|
|
128
133
|
return;
|
|
@@ -156,8 +161,8 @@ export default function comark(opts = {}) {
|
|
|
156
161
|
const files = await resolveProseFiles();
|
|
157
162
|
if (!files.length)
|
|
158
163
|
return null;
|
|
159
|
-
const injected = `import __comarkProse from ${JSON.stringify(VIRTUAL_COMPONENTS_ID)};\n`
|
|
160
|
-
|
|
164
|
+
const injected = `import __comarkProse from ${JSON.stringify(VIRTUAL_COMPONENTS_ID)};\n` +
|
|
165
|
+
code.replace(/\.mount\s*\(/, '.use(__comarkProse).mount(');
|
|
161
166
|
return { code: injected, map: null };
|
|
162
167
|
},
|
|
163
168
|
configureServer(server) {
|
package/package.json
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comark/vue",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Vue renderer for Comark (Components in Markdown)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"comark",
|
|
7
|
+
"markdown",
|
|
8
|
+
"mdc",
|
|
9
|
+
"renderer",
|
|
10
|
+
"streaming",
|
|
11
|
+
"vue"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://comark.dev/rendering/vue",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/comarkdown/comark/issues"
|
|
16
|
+
},
|
|
6
17
|
"repository": {
|
|
7
18
|
"type": "git",
|
|
8
19
|
"url": "git+https://github.com/comarkdown/comark.git"
|
|
9
20
|
},
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
"
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
14
29
|
"exports": {
|
|
15
30
|
".": "./dist/index.js",
|
|
16
31
|
"./vite": "./dist/vite.js",
|
|
@@ -20,15 +35,19 @@
|
|
|
20
35
|
"./render": "./dist/render.js",
|
|
21
36
|
"./components/*": "./dist/components/*.js"
|
|
22
37
|
},
|
|
23
|
-
"main": "./dist/index.js",
|
|
24
|
-
"module": "./dist/index.js",
|
|
25
|
-
"types": "./dist/index.d.ts",
|
|
26
|
-
"files": [
|
|
27
|
-
"dist"
|
|
28
|
-
],
|
|
29
38
|
"publishConfig": {
|
|
30
39
|
"access": "public"
|
|
31
40
|
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"comark": "^0.4.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@vue/compiler-core": "^3.5.32",
|
|
46
|
+
"@vue/server-renderer": "^3.5.32",
|
|
47
|
+
"vite": "^8.0.8",
|
|
48
|
+
"vitest": "^4.1.4",
|
|
49
|
+
"vue": "^3.5.32"
|
|
50
|
+
},
|
|
32
51
|
"peerDependencies": {
|
|
33
52
|
"beautiful-mermaid": "^1.1.3",
|
|
34
53
|
"katex": "^0.16.45",
|
|
@@ -46,16 +65,6 @@
|
|
|
46
65
|
"optional": true
|
|
47
66
|
}
|
|
48
67
|
},
|
|
49
|
-
"dependencies": {
|
|
50
|
-
"comark": "^0.3.1"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@vue/compiler-core": "^3.5.32",
|
|
54
|
-
"@vue/server-renderer": "^3.5.32",
|
|
55
|
-
"vite": "^8.0.8",
|
|
56
|
-
"vitest": "^4.1.4",
|
|
57
|
-
"vue": "^3.5.32"
|
|
58
|
-
},
|
|
59
68
|
"scripts": {
|
|
60
69
|
"stub": "node ../../scripts/stub.mjs",
|
|
61
70
|
"build": "tsc",
|