# 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 `...`, 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
```
When `custom-html-tags` includes a tag name, the parser:
- suppresses streaming mid‑states until `` 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 `...` 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
// 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
```
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)
```