Skip to content

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: and blob: for images, fonts and media). Inline styles are allowed; inline scripts are not — put code in .js files.
  • No network (connect-src 'none'). Use view.fetch, which is limited to your network permission.
  • No cookies, localStorage or IndexedDB. Use view.storage.
  • No access to the editor or the parent page. Everything goes through typora.view.
  • No eval or new Function. WebAssembly is allowed, and so are Workers created from your own files or blob: URLs.

Members ​

MemberForDescription
kindall"fence" (interactive fence), "renderer" (static renderer), "detail" or "file".
themeallThe current theme: { dark, vars }.
onTheme(cb)allCalled when the theme or appearance changes.
onOpen(cb)file viewers, interactive fencesThe content to show. See below.
onRender(fn)static renderersReturn { svg } or { html } for a fence. See Fence Renderers.
onSnapshot(fn)interactive fencesReturn { svg } for export and print.
onDocument(cb)detail pages{ blocks }, sent on load and after edits. Needs the document permission.
reveal(range)detail pagesSelect { blockId, from?, to? } in the editor. Needs the document permission.
insertMarkdown(md)detail pagesInsert Markdown at the caret, replacing the selection, as one undo step; resolves false when there is no caret. Needs document: "write".
setHeight(px)interactive fencesSet the frame height.
autoHeight()interactive fencesKeep the frame as tall as the page content.
setDirty(bool)file viewersMark the tab modified.
save(data)editable file viewersWrite a string, ArrayBuffer or Uint8Array to the file.
getState() / setState(value)file viewers, detail pagesUI state kept across tab switches.
settings()allPromise of your extension's settings.
storageallShared key/value storage. See below.
fetch(url, init)allNetwork through Typora. See below.
onMessage(cb) / postMessage(msg)pages resolved by mainMessages to and from your main script.
log(...args)allWrite to the extension's log in Settings ▸ Extensions.
processdetail pages, file viewersRun 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:

VariableValue
--typora-backgroundEditor background colour
--typora-foregroundText colour
--typora-panelPanel / card background
--typora-borderBorder colour
--typora-accentAccent colour
--typora-fontEditor font family
--typora-mono-fontMonospace font family
--typora-font-sizeEditor 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;
      }),
  );
});

Theme adapted from Nimbus (MIT).