marked-echarts
v0.2.0
Published
Safe, streaming-friendly ECharts fenced code blocks for Marked, browsers, Vue 3, and React.
Maintainers
Readme
marked-echarts
Safe, streaming-friendly ECharts fenced code blocks for Marked, browser applications, Vue 3, and React.
marked-echarts recognizes fully closed echarts fences containing strict JSON. Its core entry produces typed tokens and inert HTML placeholders—never scripts. Optional browser, Vue, and React entry points turn validated placeholders or source into charts.
Why this package?
- Strict JSON only: no JavaScript object literals,
eval,Function, or inline scripts. - Safe defaults for AI-generated and user-provided Markdown.
- Streaming-aware: an incomplete fence stays an ordinary Marked code token.
- Isolated Marked instances: no import-time or global
markedmutation. - Framework-independent browser hydration with resize and disposal handling.
- Vue 3 integration built on the lifecycle behavior of
vue-echarts. - React integration with SSR and Strict Mode-safe lifecycle handling.
- Separate entry points so core consumers do not load ECharts or framework code.
Install
Core Marked integration:
npm install marked marked-echartsAdd ECharts for browser hydration:
npm install marked marked-echarts echartsAdd the Vue adapter:
npm install marked marked-echarts echarts vue vue-echartsAdd the React adapter:
npm install marked marked-echarts echarts react react-domMarkdown syntax
Only a closed fenced code block whose first info-string word is echarts is recognized:
```echarts
{
"xAxis": {
"type": "category",
"data": ["Mon", "Tue", "Wed"]
},
"yAxis": { "type": "value" },
"series": [
{ "type": "bar", "data": [120, 200, 150] }
]
}
```Backtick and tilde fences, up to three leading spaces, and longer matching closing fences are supported. Configure additional language identifiers with aliases.
Marked usage
Create an isolated parser and register the returned standard MarkedExtension explicitly:
import { Marked } from 'marked'
import markedECharts from 'marked-echarts'
const parser = new Marked(markedECharts())
const html = parser.parse(markdown, { async: false })The renderer returns a safe structure containing a hidden chart container and a visible, escaped <pre> fallback. It does not parse or execute code and does not output <script> or inline styles.
Marked itself does not sanitize arbitrary Markdown HTML. If the entire Markdown input is untrusted, sanitize the complete marked.parse() result with a maintained HTML sanitizer before inserting it into the DOM. The ECharts placeholder generated by this package is inert, but that does not make unrelated Markdown HTML safe.
Custom aliases are opt-in:
const parser = new Marked(
markedECharts({ aliases: ['chart-json', 'echarts-json'] }),
)Browser runtime
Import the external stylesheet, insert the Marked output, and hydrate within a scoped root:
import { Marked } from 'marked'
import markedECharts from 'marked-echarts'
import {
hydrateMarkedECharts,
disposeMarkedECharts,
} from 'marked-echarts/browser'
import 'marked-echarts/style.css'
const parser = new Marked(markedECharts())
const root = document.querySelector<HTMLElement>('#content')!
root.innerHTML = parser.parse(markdown, { async: false })
const hydration = await hydrateMarkedECharts({ root })
// Dispose only charts inside root.
disposeMarkedECharts(root)
// Or dispose the charts returned by this hydration call.
hydration.dispose()By default, hydrateMarkedECharts() dynamically imports the echarts peer only after it finds a valid placeholder. A host can avoid that import or provide a custom ECharts build:
import * as echarts from 'echarts'
const hydration = await hydrateMarkedECharts({
root,
echarts,
renderer: 'svg',
theme: 'dark',
onError(error) {
console.error('Chart fallback remains visible', error)
},
})The runtime:
- validates every chart independently;
- avoids duplicate initialization;
- calls
setOption(..., { notMerge: true })when source on an existing placeholder changes; - uses
ResizeObserver, with a windowresizelistener fallback; - disconnects observers/listeners and disposes ECharts instances;
- restores the source fallback after parse, load, initialization, or update failure;
- returns an empty handle instead of crashing when called without a DOM.
Vue 3
The Vue entry imports Vue and vue-echarts; the core and browser entries do not. Register the ECharts modules your application needs (or import the full ECharts build), then use the component:
import 'echarts'
import { createApp, h } from 'vue'
import { MarkedECharts } from 'marked-echarts/vue'
import 'marked-echarts/style.css'
const app = createApp({
render: () =>
h(MarkedECharts, {
source: '{"series":[{"type":"bar","data":[1,2,3]}]}',
renderer: 'canvas',
theme: 'dark',
autoresize: true,
onReady: (chart) => console.log('ready', chart),
onError: (error) => console.error(error),
}),
})
app.mount('#app')Template example with streaming and slots:
<MarkedECharts :source="source" :streaming="streaming" :loading="loading">
<template #loading>Loading chart…</template>
<template #fallback="{ source }"><pre>{{ source }}</pre></template>
<template #error="{ error }">Could not render: {{ error }}</template>
</MarkedECharts>While streaming is true, the component never mounts vue-echarts; it renders the fallback slot or escaped source. When streaming ends, it validates source and mounts the chart. A supplied option takes precedence over source and is treated as already parsed/trusted JSON data.
autoresize defaults to true. Changing theme or renderer is passed through to vue-echarts, which owns ECharts initialization, smart updates, automatic resize, theme application, and unmount disposal.
React
The React entry imports React only when marked-echarts/react is imported. ECharts is initialized in a client effect, so server rendering produces an escaped source fallback without accessing window or document:
import { MarkedECharts } from 'marked-echarts/react'
import 'marked-echarts/style.css'
export function Chart({ source, streaming }) {
return (
<MarkedECharts
source={source}
streaming={streaming}
renderer="canvas"
autoresize
fallback={({ source }) => <pre>{source}</pre>}
errorFallback={({ error }) => <p>Could not render: {error.kind}</p>}
onReady={(chart) => console.log('ready', chart)}
onError={(error) => console.error(error)}
/>
)
}The adapter dynamically imports the full echarts peer by default. Pass echarts or loadECharts to use an application-managed or tree-shaken runtime:
The chart layout contains a dedicated empty ECharts host. ECharts exclusively owns descendants of that host, while React fallback and loading overlays remain sibling nodes owned by React. This prevents imperative renderer DOM updates from conflicting with React reconciliation.
import * as echarts from 'echarts/core'
import { BarChart } from 'echarts/charts'
import { GridComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import { MarkedECharts } from 'marked-echarts/react'
echarts.use([BarChart, GridComponent, CanvasRenderer])
<MarkedECharts source={source} echarts={echarts} />While streaming is true the chart is not initialized. A supplied option takes precedence over source and is treated as already parsed/trusted. Source or option updates use replacement semantics without recreating the instance; changing theme, renderer, or runtime recreates it. autoresize defaults to true and may also receive { throttle, onResize }.
Custom VNode integration
A Marked lexer → custom token → VNode renderer can consume the core API directly:
import { Marked } from 'marked'
import markedECharts, {
isEChartsToken,
parseEChartsOption,
} from 'marked-echarts'
import { h } from 'vue'
import { MarkedECharts } from 'marked-echarts/vue'
const parser = new Marked(markedECharts())
const tokens = parser.lexer(markdown)
const nodes = tokens.map((token) => {
if (isEChartsToken(token)) {
const parsed = parseEChartsOption(token.text)
return parsed.ok
? h(MarkedECharts, { option: parsed.value })
: h('pre', token.text)
}
return renderOtherToken(token)
})An unclosed echarts fence is returned by Marked as an ordinary code token, so an AI stream can keep displaying source until the complete closing fence arrives.
Public API
marked-echarts
import markedECharts, {
markedECharts as namedMarkedECharts,
isEChartsToken,
parseEChartsOption,
DEFAULT_ECHARTS_SECURITY_LIMITS,
type EChartsToken,
type MarkedEChartsOptions,
type EChartsSecurityOptions,
type ParseEChartsOptionResult,
type SafeEChartsOption,
} from 'marked-echarts'markedECharts(options?): returns a standard, side-effect-freeMarkedExtension.isEChartsToken(value): reliable runtime/type guard.parseEChartsOption(source, security?): returns{ ok: true, value, statistics }or{ ok: false, error }; it does not throw for source validation failures.EChartsToken:{ type: 'echarts'; raw; text; language }.
marked-echarts/browser
hydrateMarkedECharts(options?): asynchronously hydrates all placeholders underrootand returns chart handles plus per-chart errors.disposeMarkedECharts(root?): disposes scoped charts, or all active charts when no root is passed, and returns the count.HydrateMarkedEChartsOptions: acceptsroot,echarts,loadECharts,renderer,theme,security, andonError.
marked-echarts/vue
MarkedECharts: Vue 3 component.- Props:
source,option,streaming,autoresize,theme,renderer,loading, andsecurity. - Events:
readyanderror. - Slots:
loading,fallback, anderror.
marked-echarts/react
MarkedECharts: React component with a forwardedresize/disposehandle.- Props:
source,option,streaming,autoresize,theme,renderer,loading,security,echarts,loadECharts,className, andstyle. - Callbacks:
onReadyandonError. - Render surfaces:
fallback,loadingFallback, anderrorFallback.
Security boundaries
Default limits:
| Limit | Default |
| ---------------------------------------- | ------: |
| UTF-8 source bytes | 100,000 |
| Object/array depth | 20 |
| Object keys | 2,000 |
| Any single array | 10,000 |
| Total series | 100 |
| Aggregate data/source rows or points | 50,000 |
Override only what your application can safely afford:
const result = parseEChartsOption(source, {
strict: true,
limits: {
maxSourceBytes: 50_000,
maxDataPoints: 10_000,
},
})Always enforced:
- strict
JSON.parseand a JSON-object root; - recursive rejection of
__proto__,prototype, andconstructor; - all configured resource limits;
- structured failures with stable codes and paths; no silent deletion.
Strict mode is on by default and additionally rejects HTTP(S), data, blob, file, protocol-relative, image://, and javascript: resource-shaped strings; string-backed image fields; HTML formatter markup; and tooltip.extraCssText. Setting strict: false relaxes only those resource/HTML/style checks. It never enables JavaScript syntax or dangerous keys.
The parser validates data, not the full semantic ECharts schema. Keep ECharts and its extensions updated, restrict which chart modules you register when possible, and apply your application's usual DOM sanitization to the complete Markdown output. See SECURITY.md.
Error fallback
Do not discard parse errors. Keep source visible and use the error code for UI messaging:
const result = parseEChartsOption(source)
if (!result.ok) {
showCodeFallback(source, result.error)
}The browser runtime does this automatically. The Vue and React components render their error fallback, or source by default, and emit the same structured parse error.
CSP
The package requires no unsafe-eval and generates no executable or inline script. The core renderer also emits no inline style. Import marked-echarts/style.css from your normal CSS pipeline.
vue-echarts 8 can work with strict CSP; consult its current CSP guidance if you target browsers without constructable stylesheet support. Your ECharts features and surrounding application may have additional img-src, font-src, style-src, or network requirements that are outside this package.
Compatibility
| Package/runtime | Supported | Locally verified | | --------------- | ------------------ | ------------------------- | | Marked | 16, 17, 18 | 18.0.10 | | ECharts | 6.x | 6.1.0 | | React | 18.2+, 19.x | 19.2.8 | | Vue | 3.3+ | 3.5.41 | | vue-echarts | 8.1+ | 8.1.0 | | Node.js | 20+ | 22.17 | | Browser output | Modern ESM, ES2022 | jsdom + build smoke tests |
CI runs the tokenizer suite against Marked 16, 17, and 18 and the React client/SSR suites against React 18 and 19. Browser and React resizing use ResizeObserver when available and a window resize fallback otherwise.
Compared with marked-extension-echart
| | marked-echarts | marked-extension-echart |
| --------------- | -------------------------------- | ------------------------------------ |
| Input | Strict JSON | JSON or JavaScript object literal |
| Renderer output | Inert placeholder/source | Inline initialization script |
| CSP | No inline script/eval required | Script execution required |
| Streaming | Requires a complete closer | Not designed as a streaming contract |
| Runtime | Browser, Vue, and React entries | Renderer-managed script |
| Safety limits | Depth/size/count/resource policy | Not provided |
This project independently implements the behavior described here. No third-party source code was copied.
Development
npm install
npm run qualitySee CONTRIBUTING.md for focused commands and release checks.
License and acknowledgements
MIT © 2026 marked-echarts contributors.
Acknowledgements:
- Marked, MIT, for its documented extension API.
- Apache ECharts, Apache-2.0.
- vue-echarts, MIT.
marked-extension-echart, MIT, for the priorechartsfenced-block syntax and lessons around script-based rendering. Syntax was referenced; source code was not reused.
