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.
290 lines
13 KiB
Markdown
290 lines
13 KiB
Markdown
---
|
||
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.
|