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:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,289 @@
---
description: Use the parser API to hook into tokens, AST transforms, and markdown-it customization before markstream-vue renders nodes.
---
# Parser — API Deep Dive
This page extracts the most important functions, types and helpers from `packages/markdown-parser` for a focused API reference.
## Main functions
### `getMarkdown(msgId?, options?)`
Create a configured `markdown-it-ts` instance.
Parameters:
- `msgId` (string | optional): identifier for this instance
- `options` (GetMarkdownOptions | optional): configuration options
Options include:
- `plugin`: list of Markdown-it plugins
- `apply`: functions to mutate the `MarkdownIt` instance
- `i18n`: translator map or function
- `customHtmlTags`: custom HTML-like tags treated as common during streaming mid‑state handling (e.g. `['thinking']`)
### `parseMarkdownToStructure(content, md, options?)`
Parse a Markdown string into a streaming-friendly AST used by the renderer.
Parameters:
- `content` (string): markdown to parse
- `md` (MarkdownItCore): a markdown-it-ts instance created by `getMarkdown()`
- `options` (ParseOptions, optional): contains transform hooks described below
> Tip for custom components: for simple HTML‑like tags such as `<thinking>`, prefer the built‑in `customHtmlTags` / `custom-html-tags` allowlist so the parser emits custom nodes directly. Use `preTransformTokens` only when you need to reshape `content`/`attrs`. See [custom component parsing](/guide/advanced#custom-component-parsing) for details.
Returns: `ParsedNode[]`
### ParseOptions: `final` (end-of-stream mode)
In streaming contexts (SSE / AI chat), the parser intentionally emits some **mid-state (`loading`) nodes** to reduce flicker and keep rendering stable while content is incomplete, for example:
- Unclosed block math (starts with `$$` but the closing `$$` hasn’t arrived yet)
- Unclosed code fences (only the opening ``` has arrived)
When you know the message is complete, switch the parser to “final” mode:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure(buffer, md, { final: true })
```
What `final: true` does:
- Disables mid-state loading behavior for unclosed constructs (so trailing `$$...` won’t produce perpetual `math_block/loading` nodes).
- Treats EOF as an implicit closing fence so code blocks don’t get stuck in loading.
### `processTokens(tokens)`
Converts raw markdown-it tokens into a processed token list ready for the AST phase.
### `parseInlineTokens(tokens, content?, preToken?, options?)`
Parse inline tokens into inline nodes usable by the renderer.
## Configuration & helpers
### `setDefaultMathOptions(options)`
Set global math rendering options for KaTeX. Example:
```ts
setDefaultMathOptions({
commands: ['infty', 'perp'],
escapeExclamation: true
})
```
### Heuristics and utilities
- `isMathLike(content)` — heuristic to detect whether a string looks like math
- `findMatchingClose(src, startIdx, open, close)` — find matching delimiter
- `parseFenceToken(token)` — parse code fence into `CodeBlockNode`
- `normalizeStandaloneBackslashT(content, options?)` — normalize backslash-t sequences in math content
## Parse hooks (advanced)
When calling `parseMarkdownToStructure` you can pass `ParseOptions` with these hooks:
- `preTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — operate immediately after the `markdown-it` parser runs
- `postTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — transform tokens after internal fixes
If you need to reshape the AST, post-process the returned `ParsedNode[]` and pass it to `MarkdownRender` via the `nodes` prop.
These hooks are also available via `parseOptions` prop on the `MarkdownRender` component (applies only when using `content` instead of `nodes`).
### Unknown HTML-like tags (default behavior)
Non-standard HTML-like tags (for example `<question>`) render as raw HTML elements once complete. Incomplete or malformed fragments are kept as **literal text** so they do not swallow surrounding content.
If you want a custom tag to be emitted as a custom node (so it can be mapped via `setCustomComponents` with parsed attrs/content), add it to `customHtmlTags`.
### ParseOptions: `requireClosingStrong`
`requireClosingStrong` (boolean | optional) controls how the parser treats unmatched `**` strong delimiters inside inline content. Default: `false` (streaming-friendly).
- **true**: The parser requires a matching closing `**` to create a strong node. Unclosed `**` are left as plain text. This is the recommended, strict mode for non-interactive rendering and avoids incorrect strong parsing inside constructs like link text (for example, `[**cxx](xxx)`).
- **false**: The parser allows mid-state/unfinished `**` (useful for editor live-preview scenarios), which can produce a temporary strong node even when the closing `**` is missing.
Example — strict parsing (recommended for non-streaming):
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('[**cxx](xxx)', md, { requireClosingStrong: true })
// the text `[**cxx](xxx)` will be preserved without creating a dangling strong node
```
Example — editor-friendly parsing:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('[**cxx](xxx)', md, { requireClosingStrong: false })
// allows creating a temporary/"mid-state" strong node for live-edit previews
```
### ParseOptions: `validateLink`
`validateLink` (`(url: string) => boolean` | optional) controls whether a parsed link is emitted as a `link` node.
- Returns **true**: emit a normal `link` node.
- Returns **false**: downgrade the link to plain text (keep link text only).
Priority and integration:
- `parseMarkdownToStructure(..., { validateLink })` has highest priority.
- If omitted, the parser reuses the rule from the MarkdownIt instance when available (for example via `md.set({ validateLink })` or `md.options.validateLink`).
Example — block unsafe schemes while keeping normal https links:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown('safe-links')
md.set?.({
validateLink: (url: string) => !/^\s*javascript:/i.test(url.trim()),
})
const nodes = parseMarkdownToStructure(
'[safe](https://example.com) [unsafe](javascript:alert(1))',
md,
{ final: true },
)
// "safe" is emitted as a link node; "unsafe" is emitted as plain text
```
### Stray `$$` delimiters (empty math)
Sometimes streams contain an accidental `$$` sequence, e.g.
```md
adasd $$ dasdsa
dasdsad
```
If `$$` is misinterpreted as a math delimiter with empty content, the renderer can appear “stuck” because there is no math content to render. The parser now treats empty `$$` as plain text to avoid perpetual loading spinners.
### Streaming inline HTML mid-states (`html_inline`)
For streaming/chat/editor scenarios, inline HTML often arrives in partial chunks. The parser applies a conservative mid‑state strategy to reduce flicker while still rendering stable HTML as soon as possible.
Behavior:
- **Suppress incomplete tags** in text tokens until `>` arrives. Examples of suppressed fragments: `<span class="a"`, trailing `<`, `</sp`, trailing `</`. These fragments are not emitted as plain text nodes to avoid jitter.
- **Recognize complete open tags** for common HTML tags (conservative allowlist). Once `<tag ...>` is complete, it becomes an `html_inline` node.
- **If no explicit closing tag exists yet**, the parser:
- Treats all following inline tokens as that tag’s inner content.
- Auto‑appends `</tag>` to `content/raw` so the fragment is valid HTML for rendering.
- Marks the node as `loading: true` and `autoClosed: true` to indicate the source is still incomplete.
- **When the real closing tag arrives**, `autoClosed` disappears and `loading` becomes `false`.
Extending the allowlist:
To apply the same mid‑state suppression for custom tags (for example `<thinking>`), pass `customHtmlTags` when creating the markdown instance:
```ts
const md = getMarkdown('chat', { customHtmlTags: ['thinking'] })
```
Emitting custom nodes:
If you want those tags to become custom node types (so `setCustomComponents` can map them directly), also pass `customHtmlTags` in `ParseOptions` or use the `custom-html-tags` prop on `MarkdownRender` (which wires this automatically):
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown('chat', { customHtmlTags: ['thinking'] })
const nodes = parseMarkdownToStructure(source, md, { customHtmlTags: ['thinking'] })
```
Renderer note:
- `HtmlInlineNode` renders raw text only when `loading === true` and `autoClosed !== true`.
Auto‑closed mid‑states still render HTML (via `innerHTML`) but keep `loading=true` for UX/state.
## Types
A condensed list of exported types to reference in your code:
- `CodeBlockNode`, `GetMarkdownOptions`, `HeadingNode`, `ListItemNode`, `ListNode`, `MathOptions`, `ParagraphNode`, `ParsedNode`, `ParseOptions`, etc.
Use `import type { ParsedNode, CodeBlockNode } from 'stream-markdown-parser'` in your TypeScript code.
## Plugins & Defaults
This package includes a set of parsing helpers and convenience plugins for common flows (for example: footnotes, task checkboxes, sub/sup/mark). Note that emoji handling is no longer enabled by default — consumers who want emoji support should register the emoji plugin explicitly.
You can add custom plugins in several ways:
- Pass plugins to `getMarkdown` via the `plugin` option.
- Use `apply` functions in `getMarkdown` options to mutate the returned `MarkdownIt` instance.
- When using the `MarkdownRender` component, use the `customMarkdownIt` prop to receive and mutate the `MarkdownIt` instance used for that renderer.
Example — enable emoji via the component prop:
```vue
<script setup lang="ts">
import type { MarkdownIt } from 'markdown-it-ts'
import { full as markdownItEmoji } from 'markdown-it-emoji'
import MarkdownRender from 'markstream-vue'
function enableEmoji(md: MarkdownIt) {
md.use(markdownItEmoji)
return md
}
</script>
<template>
<MarkdownRender :content="source" :custom-markdown-it="enableEmoji" />
</template>
```
## Examples
Use the playground to test custom‑tag parsing quickly. For simple tags like `<thinking>`, pass `customHtmlTags` (or `custom-html-tags` on the component) so the parser emits `type: 'thinking'` nodes automatically, then map them via `setCustomComponents`.
For full details and more examples, see `packages/markdown-parser/README.md` in the repository.
Try this — quick test of a parse option:
```ts
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('Hello **world**', md)
console.log(nodes)
// Render with <MarkdownRender :nodes="nodes" />
```
### Custom components and tag-like elements
Tag-like custom elements (for example `<MyWidget ...>...</MyWidget>`) produce complex `html_block`/inline token shapes that are often difficult to reconstruct reliably from the parsed AST using simple regex or string splicing. To reduce maintenance cost and avoid brittle post-processing, we recommend extracting those raw component strings before feeding the remaining content to the Markdown parser and rendering the extracted parts separately as Vue components.
Recommended approach:
- Pre-scan the incoming Markdown content and extract custom-component blocks into a small map keyed by an id (or placeholder).
- Replace the original markup in the Markdown string with a stable placeholder token (for example `[[CUSTOM:1]]`).
- Let the Markdown parser run on the placeholder-bearing content and then render the AST as usual.
- In your rendering layer, render placeholders by looking up the extracted component string and mounting the corresponding Vue component (or compile it via a lightweight renderer).
Benefits:
- Avoids brittle AST post-processing for nested/tag-like HTML.
- Keeps the Markdown parsing focused on Markdown semantics.
- Allows you to control hydration and scope for custom components separately.
Example (simple sketch):
```ts
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
// 1) extract custom tags
const extracted = new Map<string, string>()
let id = 1
const contentWithPlaceholders = source.replace(/<MyWidget[\s\S]*?<\/MyWidget>/g, (m) => {
const key = `[[CUSTOM:${id++}]]`
extracted.set(key, m)
return key
})
// 2) parse the markdown with placeholders
const md = getMarkdown()
const nodes = parseMarkdownToStructure(contentWithPlaceholders, md)
// 3) render: when you encounter a placeholder node, mount your extracted component
// Example pseudo-Vue render logic:
// if (node.type === 'text' && extracted.has(node.content)) {
// return h(CustomWrapper, { raw: extracted.get(node.content) })
// }
```
### Thinking blocks and small inline-rich fragments
If you only need lightweight rendering for short "thinking" fragments (for example AI assistant thinking traces), extract those fragments and parse them separately, then render with a dedicated `MarkdownRender` instance. Keep complex tag-like custom components extracted as shown above so they don’t get mixed into the main AST.
This hybrid approach minimizes fragile string-manipulation of the Markdown AST while giving you full control over custom component hydration and rendering scope.