Files
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
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.
2026-06-22 11:32:57 +08:00

169 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Advanced parser hooks & low-level customization
This page is for low-level parser work: token transforms, AST reshaping, and custom node pipelines.
If you only need one of these common tasks, start elsewhere first:
- Replace a built-in renderer such as `image`, `code_block`, or `mermaid`: [Override Built-in Components](/guide/component-overrides)
- Support trusted tags such as `thinking`: [Custom Tags & Advanced Components](/guide/custom-components)
- Tune behavior without replacing renderers: [Props & Options](/guide/props)
## parseOptions
`parseOptions` can be passed to `MarkdownRender` or used directly with `parseMarkdownToStructure`.
- `preTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — mutate tokens immediately after the `markdown-it` parse
- `postTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — further token transforms
If you need to reshape the AST, post-process the returned `ParsedNode[]` and pass it to `MarkdownRender` via the `nodes` prop.
### Example: custom HTML-like tags (recommended)
For simple custom tags like `<thinking>...</thinking>`, you no longer need to normalize the source or rewrite tokens. Just opt the tag into the allowlist and register a component:
```ts
import { setCustomComponents } from 'markstream-vue'
import ThinkingNode from './ThinkingNode.vue'
setCustomComponents('docs', { thinking: ThinkingNode })
```
```vue
<MarkdownRender
custom-id="docs"
:custom-html-tags="['thinking']"
:content="markdown"
/>
```
When `custom-html-tags` includes a tag name, the parser:
- suppresses streaming mid‑states until `<tag ...>` is complete,
- emits a `CustomComponentNode` with `type: 'thinking'` (the tag name, no angle brackets), plus `content`, optional `attrs`, and `loading/autoClosed` flags.
## Custom component parsing
The built‑in custom tag pipeline above handles most “component‑like” tags (inline or block).
Hooks are still useful when you need to reshape the node — for example, to strip wrappers, merge adjacent blocks, or map attributes:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
function preTransformTokens(tokens: MarkdownToken[]) {
return tokens.map((t) => {
if (t.type === 'html_block' && t.tag === 'thinking')
return { ...t, content: String(t.content ?? '').replace(/<\/?thinking[^>]*>/g, '').trim() }
return t
})
}
const md = getMarkdown()
const nodes = parseMarkdownToStructure(markdown, md, { preTransformTokens })
```
Alternative flows (pick what fits your pipeline):
- Have the backend split `thinking` into its own field/type; render `thinking` with one `MarkdownRender` and the remaining content with another, so the parser never sees raw custom HTML inline.
- Replace custom blocks with placeholders before parsing: capture `<thinking>...</thinking>` via regex, render the cleaned body with `MarkdownRender`, then swap placeholders back to your custom component. When `thinking` is always at the top, you can also slice the head section out for dedicated rendering.
## setCustomComponents(id, mapping)
- Use `setCustomComponents('docs', { thinking: ThinkingComponent })` to scope to `MarkdownRender` instances with `custom-id="docs"`.
- Call `removeCustomComponents` to clean up mappings and avoid memory leaks in single-page apps.
## Scoped example
```vue
<MarkdownRender content="..." custom-id="docs" />
// In setup
setCustomComponents('docs', { thinking: ThinkingNode })
```
Advanced hooks are a powerful way to add domain-specific grammar to Markdown without changing the core parser.
### Typewriter prop
`MarkdownRender` accepts a `typewriter` boolean prop which controls whether non-`code_block` nodes use subtle fade treatment. New nodes still get the small enter transition, and when streamed text grows in place the newly appended text fragment replays a short fade instead of dimming the whole node container. This is useful for demo UIs but may be undesirable in SSR or print/export flows where deterministic output is needed.
Example:
```vue
<MarkdownRender :content="markdown" :typewriter="false" />
```
CSS variables: `--typewriter-fade-duration` and `--typewriter-fade-ease` tune the initial enter transition; `--stream-update-fade-duration` and `--stream-update-fade-ease` tune the short replayed fade used for newly appended streamed text. When the stream-specific variables are not set, the appended-text fade falls back to the same duration and easing as `typewriter`.
## Internationalization (i18n)
By default, `getMarkdown` uses English text for UI elements (e.g., "Copy" button in code blocks). You can customize these texts by providing an `i18n` option:
**Using a translation map:**
```ts
import { getMarkdown } from 'markstream-vue'
const md = getMarkdown('editor-1', {
i18n: {
'common.copy': '复制',
}
})
```
**Using a translation function:**
```ts
import { getMarkdown } from 'markstream-vue'
import { useI18n } from 'vue-i18n' // or any i18n library
const { t } = useI18n()
const md = getMarkdown('editor-1', {
i18n: (key: string) => t(key)
})
```
**Overriding default texts globally:**
If you just want to change some default UI texts (e.g., "Copy" → "复制") without setting up full i18n, use `setDefaultI18nMap` to replace the fallback translations:
```ts
import { setDefaultI18nMap } from 'markstream-vue'
setDefaultI18nMap({
'common.copy': '复制',
'common.copied': '已复制',
'common.decrease': '减少',
'common.reset': '重置',
'common.increase': '增加',
'common.expand': '展开',
'common.collapse': '收起',
'common.preview': '预览',
'common.source': '源码',
'common.export': '导出',
'common.open': '打开',
'common.zoomIn': '放大',
'common.zoomOut': '缩小',
'common.resetZoom': '重置缩放',
'image.loadError': '图片加载失败',
'image.loading': '图片加载中...',
})
```
This is useful when:
- You don't need language switching but prefer different wording
- You want Chinese (or other language) UI by default
- You're not using `vue-i18n` but still want custom UI texts
**Default translations:**
- `common.copy`: "Copy" — Used in code block copy buttons
- `common.copied`: "Copied" — Shown after copying
- `image.loadError`: "Image failed to load" — Image fallback text
- `image.loading`: "Loading image..." — Shown while loading
This design keeps the markdown utilities pure and free from global side effects, allowing you to integrate with any i18n solution or provide static translations.
Try this — minimal example using parseOptions and a custom component registration:
```ts
import { getMarkdown, parseMarkdownToStructure, setCustomComponents } from 'markstream-vue'
const md = getMarkdown()
setCustomComponents('docs', { thinking: ThinkingNode })
const nodes = parseMarkdownToStructure('[[CUSTOM:1]]', md)
```