e0a5f57793
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
643 lines
16 KiB
Markdown
643 lines
16 KiB
Markdown
# Vue 2 Components & API
|
|
|
|
markstream-vue2 provides the same powerful components as markstream-vue, but built for Vue 2. All components are compatible with Vue 2.6+ (with `@vue/composition-api`) and Vue 2.7+.
|
|
|
|
## TypeScript exports
|
|
|
|
`markstream-vue2` exports renderer and component prop interfaces:
|
|
|
|
```ts
|
|
import type {
|
|
CodeBlockNodeProps,
|
|
D2BlockNodeProps,
|
|
InfographicBlockNodeProps,
|
|
MermaidBlockNodeProps,
|
|
NodeRendererProps,
|
|
PreCodeNodeProps,
|
|
} from 'markstream-vue2'
|
|
import type { CodeBlockNode } from 'stream-markdown-parser'
|
|
```
|
|
|
|
Notes:
|
|
- `NodeRendererProps` matches `<MarkdownRender>` props.
|
|
- `CodeBlockNodeProps`, `MermaidBlockNodeProps`, `D2BlockNodeProps`, `InfographicBlockNodeProps`, and `PreCodeNodeProps` all use `CodeBlockNode` for `node` (use `language: 'mermaid'` / `language: 'd2'` / `language: 'd2lang'` / `language: 'infographic'` to route specialized renderers).
|
|
|
|
## Main Component: MarkdownRender
|
|
|
|
The primary component for rendering markdown content in Vue 2.
|
|
|
|
### Props
|
|
|
|
`MarkdownRender` in Vue 2 mirrors the Vue 3 renderer props. In templates, use kebab-case.
|
|
|
|
#### Core props
|
|
|
|
| Prop | Type | Default | Description |
|
|
|------|------|---------|-------------|
|
|
| `content` | `string` | - | Markdown content to render |
|
|
| `nodes` | `BaseNode[]` | - | Pre-parsed AST nodes (typically `ParsedNode[]` from the parser) |
|
|
| `custom-id` | `string` | - | Identifier for scoping custom components and CSS (`[data-custom-id="..."]`) |
|
|
| `final` | `boolean` | `false` | Marks the input as end-of-stream; stops emitting streaming `loading` nodes |
|
|
| `parse-options` | `ParseOptions` | - | Parser options and token hooks (only when `content` is provided) |
|
|
| `custom-html-tags` | `string[]` | - | HTML-like tags emitted as custom nodes (e.g. `thinking`) |
|
|
| `custom-markdown-it` | `(md: MarkdownIt) => MarkdownIt` | - | Customize the internal MarkdownIt instance |
|
|
| `debug-performance` | `boolean` | `false` | Log parse/render timing and virtualization stats (dev only) |
|
|
| `is-dark` | `boolean` | `false` | Theme flag forwarded to heavy nodes; adds `.dark` to the root container |
|
|
| `index-key` | `number \| string` | - | Key prefix when rendering multiple instances in lists |
|
|
| `typewriter` | `boolean` | `true` | Enable the non-code-node enter transition |
|
|
| `show-tooltips` | `boolean` | `true` | Global tooltip switch for `LinkNode` and code block nodes |
|
|
|
|
#### Streaming & heavy-node toggles
|
|
|
|
| Prop | Default | Description |
|
|
|------|---------|-------------|
|
|
| `render-code-blocks-as-pre` | `false` | Render non-Mermaid/Infographic/D2 code blocks as `<pre><code>` |
|
|
| `code-block-stream` | `true` | Stream code block updates as content arrives |
|
|
| `viewport-priority` | `true` | Defer heavy work (Monaco/Mermaid/D2/KaTeX) until near viewport |
|
|
| `defer-nodes-until-visible` | `true` | Render heavy nodes as placeholders until visible (non-virtualized mode only) |
|
|
|
|
#### Performance (virtualization & batching)
|
|
|
|
| Prop | Default | Description |
|
|
|------|---------|-------------|
|
|
| `max-live-nodes` | `320` | Max fully rendered nodes kept in DOM (set `0` to disable virtualization) |
|
|
| `live-node-buffer` | `60` | Overscan buffer around the focus range |
|
|
| `batch-rendering` | `true` | Incremental batch rendering when virtualization is disabled |
|
|
| `initial-render-batch-size` | `40` | Nodes rendered immediately before batching starts |
|
|
| `render-batch-size` | `80` | Nodes rendered per batch tick |
|
|
| `render-batch-delay` | `16` | Extra delay (ms) before each batch after rAF |
|
|
| `render-batch-budget-ms` | `6` | Time budget (ms) before adaptive batch sizes shrink |
|
|
| `render-batch-idle-timeout-ms` | `120` | Timeout (ms) for `requestIdleCallback` slices |
|
|
|
|
#### Global code block options
|
|
|
|
| Prop | Type | Description |
|
|
|------|------|-------------|
|
|
| `code-block-dark-theme` | `any` | Monaco dark theme object forwarded to every `CodeBlockNode` |
|
|
| `code-block-light-theme` | `any` | Monaco light theme object forwarded to every `CodeBlockNode` |
|
|
| `code-block-monaco-options` | `Record<string, any>` | Options forwarded to `stream-monaco`, including diff hover-action settings like `diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, and `onDiffHunkAction` |
|
|
| `code-block-min-width` | `string \| number` | Min width forwarded to `CodeBlockNode` |
|
|
| `code-block-max-width` | `string \| number` | Max width forwarded to `CodeBlockNode` |
|
|
| `code-block-props` | `Record<string, any>` | Extra props forwarded to every `CodeBlockNode` |
|
|
| `mermaid-props` | `Partial<Omit<MermaidBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to Mermaid fences and custom `mermaid` renderers |
|
|
| `d2-props` | `Partial<Omit<D2BlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to D2 fences and custom `d2` renderers |
|
|
| `infographic-props` | `Partial<Omit<InfographicBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to infographic fences and custom `infographic` renderers |
|
|
| `themes` | `string[]` | Theme list forwarded to `stream-monaco` |
|
|
|
|
#### Heavy renderer prop forwarding
|
|
|
|
`MarkdownRender` can tune heavy blocks directly:
|
|
|
|
```vue
|
|
<MarkdownRender
|
|
:content="markdown"
|
|
:viewport-priority="true"
|
|
:mermaid-props="{
|
|
showHeader: false,
|
|
renderDebounceMs: 180,
|
|
previewPollDelayMs: 500
|
|
}"
|
|
:d2-props="{ progressiveIntervalMs: 500 }"
|
|
:infographic-props="{ showHeader: false }"
|
|
/>
|
|
```
|
|
|
|
Streaming notes:
|
|
- Keep `viewport-priority` enabled to prevent offscreen Mermaid / Monaco / D2 work from running while text is still streaming.
|
|
- For high-frequency SSE, prefer passing `nodes` instead of reparsing the full `content` string every chunk.
|
|
- Common Mermaid tuning keys: `renderDebounceMs`, `contentStableDelayMs`, `previewPollDelayMs`, `previewPollMaxDelayMs`, `previewPollMaxAttempts`.
|
|
|
|
#### Events
|
|
|
|
- `@copy`, `@handleArtifactClick`, `@click`, `@mouseover`, `@mouseout`
|
|
|
|
### Usage
|
|
|
|
```vue
|
|
<script>
|
|
import MarkdownRender from 'markstream-vue2'
|
|
|
|
export default {
|
|
components: { MarkdownRender },
|
|
data() {
|
|
return {
|
|
markdown: '# Hello Vue 2!'
|
|
}
|
|
},
|
|
methods: {
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<MarkdownRender
|
|
custom-id="docs"
|
|
:content="markdown"
|
|
:max-live-nodes="150"
|
|
/>
|
|
</template>
|
|
```
|
|
|
|
## Code Block Components
|
|
|
|
### MarkdownCodeBlockNode
|
|
|
|
Lightweight code highlighting using Shiki.
|
|
|
|
```vue
|
|
<script>
|
|
import { MarkdownCodeBlockNode } from 'markstream-vue2'
|
|
|
|
export default {
|
|
components: { MarkdownCodeBlockNode },
|
|
data() {
|
|
return {
|
|
codeNode: {
|
|
type: 'code_block',
|
|
language: 'javascript',
|
|
code: 'const hello = "world"',
|
|
raw: 'const hello = "world"'
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
handleCopy() {
|
|
alert('Code copied!')
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="markstream-vue">
|
|
<MarkdownCodeBlockNode
|
|
:node="codeNode"
|
|
:show-copy-button="true"
|
|
@copy="handleCopy"
|
|
>
|
|
<template #header-right>
|
|
<span class="lang-badge">{{ codeNode.language }}</span>
|
|
</template>
|
|
</MarkdownCodeBlockNode>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
### CodeBlockNode
|
|
|
|
Feature-rich Monaco-powered code blocks.
|
|
|
|
```vue
|
|
<script>
|
|
import { CodeBlockNode } from 'markstream-vue2'
|
|
|
|
export default {
|
|
components: { CodeBlockNode },
|
|
data() {
|
|
return {
|
|
codeNode: {
|
|
type: 'code_block',
|
|
language: 'typescript',
|
|
code: 'const greeting: string = "Hello"',
|
|
raw: 'const greeting: string = "Hello"'
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
handleCopy(code) {
|
|
console.log('Code copied:', code)
|
|
},
|
|
handlePreviewCode(artifact) {
|
|
console.log('Preview code:', artifact)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="markstream-vue">
|
|
<CodeBlockNode
|
|
:node="codeNode"
|
|
:monaco-options="{ fontSize: 14, theme: 'vs-dark' }"
|
|
:stream="true"
|
|
@copy="handleCopy"
|
|
@preview-code="handlePreviewCode"
|
|
/>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## Math Components
|
|
|
|
### MathBlockNode
|
|
|
|
Renders block-level math formulas with KaTeX.
|
|
|
|
```vue
|
|
<script>
|
|
import { MathBlockNode } from 'markstream-vue2'
|
|
|
|
export default {
|
|
components: { MathBlockNode },
|
|
data() {
|
|
return {
|
|
mathNode: {
|
|
type: 'math_block',
|
|
content: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}',
|
|
raw: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="markstream-vue">
|
|
<MathBlockNode
|
|
:node="mathNode"
|
|
/>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
### MathInlineNode
|
|
|
|
Renders inline math formulas.
|
|
|
|
```vue
|
|
<script>
|
|
import { MathInlineNode } from 'markstream-vue2'
|
|
|
|
export default {
|
|
components: { MathInlineNode },
|
|
data() {
|
|
return {
|
|
inlineMathNode: {
|
|
type: 'math_inline',
|
|
content: 'E = mc^2',
|
|
raw: 'E = mc^2'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="markstream-vue">
|
|
<p>
|
|
The formula is:
|
|
<MathInlineNode :node="inlineMathNode" />
|
|
</p>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## Mermaid Diagrams
|
|
|
|
### MermaidBlockNode
|
|
|
|
Progressive Mermaid diagram rendering.
|
|
|
|
#### Quick reference
|
|
- **Tooltip control**: `showTooltips` defaults to `true`; set `false` to disable header action tooltips.
|
|
|
|
```vue
|
|
<script>
|
|
import { MermaidBlockNode } from 'markstream-vue2'
|
|
|
|
export default {
|
|
components: { MermaidBlockNode },
|
|
data() {
|
|
return {
|
|
mermaidNode: {
|
|
type: 'code_block',
|
|
language: 'mermaid',
|
|
code: `graph TD
|
|
A[Start] --> B{Is it working?}
|
|
B -->|Yes| C[Great!]`,
|
|
raw: ''
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
onExport(ev) {
|
|
console.log('Mermaid SVG:', ev.svgString)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="markstream-vue">
|
|
<MermaidBlockNode
|
|
:node="mermaidNode"
|
|
:is-strict="true"
|
|
:render-debounce-ms="180"
|
|
:preview-poll-delay-ms="500"
|
|
:show-tooltips="true"
|
|
@export="onExport"
|
|
/>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
Useful streaming props:
|
|
- `render-debounce-ms`
|
|
- `content-stable-delay-ms`
|
|
- `preview-poll-delay-ms`
|
|
- `preview-poll-max-delay-ms`
|
|
- `preview-poll-max-attempts`
|
|
|
|
## D2 Diagrams
|
|
|
|
### D2BlockNode
|
|
|
|
Progressive D2 diagram rendering with source fallback.
|
|
|
|
```vue
|
|
<script>
|
|
import { D2BlockNode } from 'markstream-vue2'
|
|
|
|
export default {
|
|
components: { D2BlockNode },
|
|
data() {
|
|
return {
|
|
d2Node: {
|
|
type: 'code_block',
|
|
language: 'd2',
|
|
code: `direction: right
|
|
Client -> API: request
|
|
API -> DB: query`,
|
|
raw: ''
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="markstream-vue">
|
|
<D2BlockNode
|
|
:node="d2Node"
|
|
:progressive-interval-ms="600"
|
|
/>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## Utility Functions
|
|
|
|
### setCustomComponents
|
|
|
|
Register custom node renderers for specific markdown nodes.
|
|
|
|
```js
|
|
import { setCustomComponents } from 'markstream-vue2'
|
|
|
|
// Define custom component
|
|
const CustomHeading = {
|
|
name: 'CustomHeading',
|
|
props: ['node', 'indexKey', 'customId'],
|
|
render(h) {
|
|
const level = this.node.level || 1
|
|
return h(`h${level}`, {
|
|
class: 'custom-heading',
|
|
attrs: { 'data-custom-id': this.customId }
|
|
}, this.node.children.map(c => c.content))
|
|
}
|
|
}
|
|
|
|
// Register globally
|
|
setCustomComponents('docs', {
|
|
heading: CustomHeading
|
|
})
|
|
```
|
|
|
|
### getMarkdown
|
|
|
|
Get a configured markdown-it instance.
|
|
|
|
```js
|
|
import { getMarkdown } from 'markstream-vue2'
|
|
|
|
const md = getMarkdown('my-msg-id', {
|
|
html: true,
|
|
linkify: true,
|
|
typographer: true
|
|
})
|
|
|
|
const tokens = md.parse('# Hello World')
|
|
```
|
|
|
|
### parseMarkdownToStructure
|
|
|
|
Parse markdown string to AST structure.
|
|
|
|
```js
|
|
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue2'
|
|
|
|
const md = getMarkdown()
|
|
const nodes = parseMarkdownToStructure('# Title\n\nContent here...', md)
|
|
|
|
// Use with MarkdownRender
|
|
// <MarkdownRender :nodes="nodes" />
|
|
```
|
|
|
|
### enableKatex / enableMermaid / enableD2
|
|
|
|
(Re)enable feature loaders for KaTeX, Mermaid, and D2. Default loaders are already on; call these only if you disabled them earlier or want to override the loader (for example, using a CDN build).
|
|
|
|
```js
|
|
import { enableD2, enableKatex, enableMermaid } from 'markstream-vue2'
|
|
|
|
// Enable KaTeX loader
|
|
enableKatex()
|
|
|
|
// Enable Mermaid loader
|
|
enableMermaid()
|
|
|
|
// Enable D2 loader
|
|
enableD2()
|
|
```
|
|
|
|
## Custom Component API
|
|
|
|
### Props Interface
|
|
|
|
All custom node components receive these props:
|
|
|
|
```ts
|
|
interface NodeComponentProps {
|
|
node: ParsedNode // The parsed node data
|
|
indexKey: number | string // Unique key for the node
|
|
customId?: string // Custom ID for scoping
|
|
isDark?: boolean // Forwarded theme flag (from MarkdownRender)
|
|
typewriter?: boolean // Forwarded typewriter flag (non-code nodes only)
|
|
loading?: boolean // Streaming/loading state (from node.loading)
|
|
}
|
|
```
|
|
|
|
### Example Custom Component
|
|
|
|
```vue
|
|
<script>
|
|
export default {
|
|
name: 'CustomParagraph',
|
|
props: {
|
|
node: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
indexKey: {
|
|
type: [Number, String],
|
|
default: 0
|
|
},
|
|
customId: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
computed: {
|
|
tag() {
|
|
return 'p'
|
|
},
|
|
classes() {
|
|
return [
|
|
'custom-paragraph',
|
|
`custom-paragraph-${this.indexKey}`
|
|
]
|
|
},
|
|
attrs() {
|
|
return {
|
|
'data-custom-id': this.customId,
|
|
'data-node-type': this.node.type
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<component
|
|
:is="tag"
|
|
:class="classes"
|
|
v-bind="attrs"
|
|
>
|
|
<slot>
|
|
<template v-for="(child, i) in node.children">
|
|
<span v-if="child.type === 'text'" :key="i">
|
|
{{ child.content }}
|
|
</span>
|
|
<!-- Handle other node types... -->
|
|
</template>
|
|
</slot>
|
|
</component>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.custom-paragraph {
|
|
line-height: 1.7;
|
|
color: #333;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## Streaming Support
|
|
|
|
markstream-vue2 supports streaming markdown content with the `loading` state on nodes:
|
|
|
|
```vue
|
|
<script>
|
|
import MarkdownRender from 'markstream-vue2'
|
|
|
|
export default {
|
|
components: { MarkdownRender },
|
|
data() {
|
|
return {
|
|
streamingContent: '',
|
|
fullContent: `# Streaming Demo
|
|
|
|
This content streams in **small chunks**.
|
|
|
|
\`\`\`javascript
|
|
console.log('Streaming...')
|
|
\`\`\`
|
|
`
|
|
}
|
|
},
|
|
mounted() {
|
|
this.startStreaming()
|
|
},
|
|
methods: {
|
|
startStreaming() {
|
|
let i = 0
|
|
const interval = setInterval(() => {
|
|
if (i < this.fullContent.length) {
|
|
this.streamingContent += this.fullContent[i]
|
|
i++
|
|
}
|
|
else {
|
|
clearInterval(interval)
|
|
}
|
|
}, 30)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<MarkdownRender :content="streamingContent" />
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## TypeScript Support
|
|
|
|
markstream-vue2 includes full TypeScript definitions. For Vue 2.6.x, configure your `tsconfig.json`:
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"types": ["@vue/composition-api", "markstream-vue2"],
|
|
"moduleResolution": "node",
|
|
"esModuleInterop": true
|
|
}
|
|
}
|
|
```
|
|
|
|
For Vue 2.7+, types are included automatically:
|
|
|
|
```ts
|
|
import type { ParsedNode } from 'markstream-vue2'
|
|
import MarkdownRender from 'markstream-vue2'
|
|
|
|
// Your component with proper typing
|
|
import { defineComponent, ref } from 'vue'
|
|
|
|
export default defineComponent({
|
|
components: { MarkdownRender },
|
|
setup() {
|
|
const markdown = ref('# Hello')
|
|
const nodes = ref<ParsedNode[]>([])
|
|
|
|
return { markdown, nodes }
|
|
}
|
|
})
|
|
```
|
|
|
|
## Differences from Vue 3 Version
|
|
|
|
The Vue 2 version maintains API compatibility with the Vue 3 version with these considerations:
|
|
|
|
1. **Composition API**: Requires Vue 2.7+ or `@vue/composition-api` for Vue 2.6.x
|
|
2. **Slots**: Use Vue 2 scoped slot syntax
|
|
3. **Event names**: Use kebab-case for event names in templates
|
|
4. **v-model**: No changes needed, works the same way
|
|
|
|
## Next Steps
|
|
|
|
- See [Vue 2 Quick Start](/guide/vue2-quick-start) for setup examples
|
|
- Explore [Vue 3 Components](/guide/components) for more component examples (API is the same)
|
|
- Check [Usage & API](/guide/usage) for advanced patterns
|