Appearance
Fence Renderers
A fence renderer turns a fenced code block of a given language into a picture or a live view:
markdown
```plantuml
Alice -> Bob: Hello
```Extension fences get the same treatment as Typora's built-in diagrams (Mermaid, flowchart, sequence): zoom, full screen, side-by-side editing, error display, and export to HTML, PDF and images. Editing the fence source stays in Typora's code editor; your renderer only produces the preview.
There are four ways to render:
| Kind | Manifest | Tier | Good for |
|---|---|---|---|
| Kroki | "kroki": "<type>" | L0 + network | PlantUML, Graphviz, D2, … with no code |
| Static | "entry", "mode": "static" (default) | L1 | Anything you can draw to SVG offline |
| Interactive | "entry", "mode": "interactive" | L1 | Live views: boards, charts with hover, maps |
| Resolver | "resolver": true | L2 | main picks the page and talks to it |
Fields
| Field | Description |
|---|---|
lang | A language or a list of them (lowercase). The first one is the default id. |
id | Local id of the renderer. Defaults to the first language. |
kroki | Kroki diagram type (lowercase letters and digits), e.g. plantuml, graphviz. |
entry | Path to the renderer page in your extension. |
mode | "static" (default) or "interactive". Ignored for Kroki. |
resolver | true to let main choose the page. Requires main. |
If another installed extension renders the same language, the user chooses which one draws it in Settings ▸ Markdown ▸ Syntax by Extension. Until they do, the one asking for the fewest permissions draws it — an offline renderer before one that sends the source to a server — and among equals the first by name. flow and sequence are always Typora's own. mermaid is drawn by Typora's built-in renderer (Pele) unless an extension draws it: the built-in renderer counts as one asking for no permissions, so an offline renderer such as the Mermaid.js extension takes over, one that sends the source to a server doesn't until the user chooses it.
Every renderer needs one of kroki, entry or resolver.
Kroki
Kroki draws most text-based diagram languages behind one HTTP API. A Kroki renderer needs no code:
json
{
"id": "someone.diagrams",
"name": "Diagrams",
"version": "1.0.0",
"permissions": { "network": ["https://kroki.io"] },
"contributes": {
"fenceRenderers": [
{ "lang": ["plantuml", "puml"], "kroki": "plantuml" },
{ "lang": ["graphviz", "dot"], "kroki": "graphviz" },
{ "lang": ["d2"], "kroki": "d2" }
],
"configuration": {
"title": "Diagrams",
"properties": {
"server": {
"type": "string",
"default": "https://kroki.io",
"title": "Server",
"description": "Diagram source is sent here to be drawn."
}
}
}
}
}- Typora sends the fence source to
<server>/<type>/svg, sanitizes the SVG it gets back, caches it by content, and reuses it for export. - The server is your extension's
serversetting, orhttps://kroki.iowhen it is empty. A self-hosted server must also be covered by yournetworkpermission — use a wildcard such ashttps://*.example.comif needed. - A Kroki renderer must declare a
networkpermission. The consent dialog tells the user that diagram source is sent to that server.
Static renderers
A static renderer is one hidden page per extension, shared by every fence in the window. Typora asks it to render a source and gets SVG or HTML back. The output is sanitized and inlined into the document like a built-in diagram, so it is cheap, scrolls smoothly, and exports as-is.
json
"fenceRenderers": [
{ "id": "chart", "lang": ["chart"], "mode": "static", "entry": "renderer.html" }
]renderer.html:
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script src="__typora/view.js"></script>
<script src="chart.js"></script>
</head>
<body></body>
</html>chart.js:
js
window.typora.view.onRender(({ source, info, lang, theme, purpose }) => {
const rows = source.trim().split("\n").map((line) => line.split(","));
// Throwing shows the message in place of the diagram.
if (!rows.length) throw new Error("No data rows.");
return { svg: drawBarChart(rows, theme) };
});The handler receives:
| Field | Description |
|---|---|
source | The fence body. |
info | The full info string, e.g. chart {height=300}. |
lang | The language (first word of the info string). |
theme | { dark, background, foreground, panel, border } — colours of the current theme. |
purpose | "view" in the editor, "export" for export and print. |
Return { svg: "<svg …>" } or { html: "…" }, or a promise of one. The handler may be async.
Sanitized output
Scripts, event handlers, <iframe>, <object>, media elements, forms, SMIL animation and every external reference are removed from the returned markup. Only #fragment references and inline data:image/… URLs survive in href, src and CSS url(). Draw everything inline.
The output also gets its own ids and styles, since it sits in the document next to other diagrams: every id is given a prefix unique to the render (with #id and url(#id) references updated to match), and rules in a <style> element only apply inside the output — text { fill: white } can't recolor the rest of the window. { html } output is wrapped in a <div> to scope its styles.
The page can use the rest of the view SDK: view.settings() for your configuration, view.fetch with a network permission, and view.process with a process permission — a renderer can draw with a server or a program on the user's computer (see the PlantUML sample below). When the user saves new settings, every fence the extension drew is drawn again, so read the settings in each onRender. The page lives as long as the extension runs in the window: a program it keeps running between renders is stopped when the extension stops.
Interactive renderers
An interactive renderer gets its own sandboxed frame for each fence, mounted while the fence is on screen. Use it when the result needs to respond to the mouse.
json
"fenceRenderers": [
{ "id": "kanban", "lang": ["kanban"], "mode": "interactive", "entry": "board.html" }
]js
const view = window.typora.view;
view.onOpen(({ source, info, lang, purpose }) => {
render(parse(source));
view.autoHeight(); // keep the frame as tall as the content
});
// Export and print need a static picture.
view.onSnapshot(() => ({ svg: toSvg(columns) }));- The frame starts 320 px tall. Call
view.setHeight(px)orview.autoHeight()to size it. - Frames are created when a fence scrolls into view and destroyed when it scrolls far away, so keep state in the fence source, or in
view.setState. - For export and print, Typora mounts the page off-screen with
purpose: "export"and calls youronSnapshothandler. Return{ svg }. Without a snapshot handler the fence can't be exported.
Resolvers
With "resolver": true, main decides which page to show for each fence and can exchange messages with it:
json
"fenceRenderers": [{ "id": "map", "lang": ["map"], "mode": "interactive", "resolver": true }],
"main": "main.js"js
export function activate(ctx) {
ctx.subscriptions.push(
typora.views.registerFenceResolver("map", {
async resolve(fence, panel) {
// fence: { source, info, purpose }
panel.entry = fence.source.length > 10_000 ? "map/large.html" : "map/index.html";
panel.height = 400;
panel.postMessage({ type: "config", style: typora.settings.get("style") });
panel.onMessage((message) => console.log("from page", message));
},
}),
);
}main is activated the first time such a fence renders (onFence:<id>). Because a document may contain many fences, prefer plain entry pages unless you need main.
In command-line export
typora export loads extensions: Kroki and static renderers render as usual, interactive renderers are asked for a snapshot. Pass --no-extensions to ignore installed extensions. When no renderer is available (the export core on Linux without a display), extension fences stay as source, like built-in diagrams.
See the Charts, Kanban and Kroki samples, Offline Diagrams (libraries and WebAssembly in a static renderer), and PlantUML (a local program or a Kroki server, chosen in the extension's options).