Appearance
View SDK
Extension pages (L1) — fence renderers, detail views and file viewers — load the View SDK, which defines window.typora.view:
html
<script src="__typora/view.js"></script>The path is relative to the root of your extension; from a page in a sub-folder use ../__typora/view.js. Typora serves the SDK under every extension, so you don't ship it.
The sandbox
Pages are loaded into a sandboxed frame on an opaque origin, with a strict Content Security Policy:
- Scripts, styles, images, fonts and media may only come from your extension's own files (plus
data:andblob:for images, fonts and media). Inline styles are allowed; inline scripts are not — put code in.jsfiles. - No network (
connect-src 'none'). Useview.fetch, which is limited to yournetworkpermission. - No cookies,
localStorageor IndexedDB. Useview.storage. - No access to the editor or the parent page. Everything goes through
typora.view. - No
evalornew Function. WebAssembly is allowed, and so are Workers created from your own files orblob:URLs.
Members
| Member | For | Description |
|---|---|---|
kind | all | "fence" (interactive fence), "renderer" (static renderer), "detail" or "file". |
theme | all | The current theme: { dark, vars }. |
onTheme(cb) | all | Called when the theme or appearance changes. |
onOpen(cb) | file viewers, interactive fences | The content to show. See below. |
onRender(fn) | static renderers | Return { svg } or { html } for a fence. See Fence Renderers. |
onSnapshot(fn) | interactive fences | Return { svg } for export and print. |
onDocument(cb) | detail pages | { blocks }, sent on load and after edits. Needs the document permission. |
reveal(range) | detail pages | Select { blockId, from?, to? } in the editor. Needs the document permission. |
insertMarkdown(md) | detail pages | Insert Markdown at the caret, replacing the selection, as one undo step; resolves false when there is no caret. Needs document: "write". |
setHeight(px) | interactive fences | Set the frame height. |
autoHeight() | interactive fences | Keep the frame as tall as the page content. |
setDirty(bool) | file viewers | Mark the tab modified. |
save(data) | editable file viewers | Write a string, ArrayBuffer or Uint8Array to the file. |
getState() / setState(value) | file viewers, detail pages | UI state kept across tab switches. |
settings() | all | Promise of your extension's settings. |
storage | all | Shared key/value storage. See below. |
fetch(url, init) | all | Network through Typora. See below. |
onMessage(cb) / postMessage(msg) | pages resolved by main | Messages to and from your main script. |
log(...args) | all | Write to the extension's log in Settings ▸ Extensions. |
process | detail pages, file viewers | Run programs. Needs the process permission. See below. |
Callbacks registered with on… return a function that unsubscribes.
onOpen
For file viewers:
js
view.onOpen(({ name, path, ext, size, readonly, bytes, state }) => { … });bytes is an ArrayBuffer with the file's contents; state is what you last passed to setState for this file.
For interactive fences:
js
view.onOpen(({ source, info, lang, purpose }) => { … });purpose is "view" in the editor and "export" when Typora renders the fence off-screen for export or print.
Theme
The SDK sets data-theme="light" or "dark" and color-scheme on <html>, and copies the editor's look into CSS custom properties, so a page can follow the theme with plain CSS:
| Variable | Value |
|---|---|
--typora-background | Editor background colour |
--typora-foreground | Text colour |
--typora-panel | Panel / card background |
--typora-border | Border colour |
--typora-accent | Accent colour |
--typora-font | Editor font family |
--typora-mono-font | Monospace font family |
--typora-font-size | Editor font size |
css
body {
font: var(--typora-font-size) var(--typora-font);
color: var(--typora-foreground);
background: transparent;
}
.card {
background: var(--typora-panel);
border: 1px solid var(--typora-border);
}view.theme.vars holds the same values. Static fence renderers also receive theme ({ dark, background, foreground, panel, border }) in onRender.
State and storage
js
const days = (await view.storage.get("days")) ?? {};
await view.storage.set("days", days);
await view.storage.remove("days");
const keys = await view.storage.keys();Storage is per extension and shared with your other pages and your main / background scripts. Values are JSON. It is removed when the extension is uninstalled.
getState() / setState() are for transient UI state (scroll position, sort order) that should survive a tab switch but not a restart.
Network
js
const response = await view.fetch("https://api.example.com/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ q: "typora" }),
});
if (response.ok) render(await response.json());view.fetch works only for origins in your network permission. body is a string. The response has status, statusText, ok, url, headers (a plain object) and text(), json(), arrayBuffer().
Programs
view.process is typora.process for pages: spawn, list, attach, info and shell, with the same handles. A page is destroyed when its tab is hidden, but the programs it started keep running; look for them when the page loads:
js
const running = (await view.process.list()).find((p) => p.name === "terminal" && p.running);
const shell = running
? await view.process.attach(running.id) // shell.output: recent output to replay
: await view.process.spawn(view.process.shell, [], { pty: { cols, rows }, name: "terminal" });
shell.onData((bytes) => term.write(bytes));The Terminal sample is a complete detail page built this way with xterm.js.
Keyboard
Unhandled key presses with ⌘ or Ctrl, and function keys, are forwarded to Typora, so app shortcuts keep working while your page has focus. Copy, cut, paste, select-all, undo and redo stay in text fields. Call event.preventDefault() in your own keydown handler to keep a shortcut for the page.
Example: a detail page
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script src="../__typora/view.js"></script>
<script src="outline.js" defer></script>
</head>
<body><ul id="list"></ul></body>
</html>js
const view = window.typora.view;
const list = document.getElementById("list");
view.onDocument(({ blocks }) => {
list.replaceChildren(
...blocks
.filter((block) => block.type === "heading")
.map((block) => {
const item = document.createElement("li");
item.textContent = block.text.replace(/^#+\s*/, "");
item.style.marginLeft = `${(block.attrs.level - 1) * 12}px`;
item.onclick = () => view.reveal({ blockId: block.id });
return item;
}),
);
});