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.
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# 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)
|
||||
```
|
||||
Reference in New Issue
Block a user