How to use Rune.
Install it, turn features on or off from one config file, and drive it with a small JavaScript API. This guide covers everything from a five-minute setup to writing your own extensions and wiring up real-time collaboration.
Installation #
Rune has zero runtime dependencies and needs no build step.
# npm
npm install @parityfox/rune-editor
# yarn
yarn add @parityfox/rune-editor
# pnpm
pnpm add @parityfox/rune-editor
Import the stylesheet once in your app entry point:
import '@parityfox/rune-editor/styles';
examples/index.html with any static server — npx serve . -p 4000. Setting up a specific bundler (Vite / webpack / Rollup / esbuild) or framework? The in-repo Installation & Setup guide covers each one.Quick start #
The same engine, five adapters. Pick your stack.
Vanilla JS (recommended)
import { createFromConfig } from '@parityfox/rune-editor';
import config from './rune.config.js';
import '@parityfox/rune-editor/styles';
const editor = createFromConfig('#app', config, {
content: '<p>Start writing…</p>',
onChange(html) { console.log(html); },
});
React
import { RuneEditor } from '@parityfox/rune-editor/react';
import config from './rune.config.js';
import '@parityfox/rune-editor/styles';
export default function App() {
return (
<RuneEditor
extensions={config.extensions}
content="<p>Hello</p>"
onChange={(html) => console.log(html)}
/>
);
}
Vue 3
<script setup>
import { RuneEditor } from '@parityfox/rune-editor/vue';
import config from './rune.config.js';
import '@parityfox/rune-editor/styles';
</script>
<template>
<RuneEditor
:extensions="config.extensions"
content="<p>Hello</p>"
@change="(html) => console.log(html)"
/>
</template>
Svelte
<script>
import { rune } from '@parityfox/rune-editor/svelte';
import config from './rune.config.js';
import '@parityfox/rune-editor/styles';
</script>
<div use:rune={{ config, content: '<p>Hello</p>', onChange: (html) => console.log(html) }}></div>
Web Component
<link rel="stylesheet" href="node_modules/@parityfox/rune-editor/styles/rune.css">
<script type="module"
src="node_modules/@parityfox/rune-editor/adapters/web-component/rune-editor.js"></script>
<rune-editor content="<p>Hello world</p>" placeholder="Start writing…"></rune-editor>
<script>
document.querySelector('rune-editor')
.addEventListener('change', (e) => console.log(e.detail)); // html string
</script>
Enable / disable features #
Everything is a single boolean in rune.config.js. Set a feature to false (or omit it) and it disappears from the bundle’s surface, the toolbar, the menus, and the keyboard shortcuts — no other file needs touching. A feature is on unless you explicitly set it to false.
// rune.config.js
const config = {
// ── Block types ────────────────────────────────
blocks: {
paragraph: true, // <p>
heading: true, // <h1>–<h3>
bulletList: true, // <ul>
orderedList: true, // <ol>
blockquote: true, // <blockquote>
codeBlock: true, // <pre><code>
horizontalRule: true, // <hr>
callout: true, // highlighted callout box
taskList: true, // checklist
videoEmbed: true, // YouTube / Vimeo embed
image: true, // <figure><img>
table: true, // <table>
toggle: true, // collapsible section
columns: true, // side-by-side columns
},
// ── Inline marks ───────────────────────────────
marks: {
bold: true, italic: true, underline: true, strike: true,
code: true, link: true, superscript: true, subscript: true,
fontSize: true, fontFamily: true,
textColor: true, textBackground: true,
highlight: true, // marker-pen highlight (<mark>)
mention: false, // @-mention autocomplete (needs editor.fetchMentions)
hashtag: false, // #tag autocomplete
},
// ── Formatting ─────────────────────────────────
formatting: {
textAlign: true, lineHeight: true,
indent: true, outdent: true, clearFormat: true,
},
// ── Plugins ────────────────────────────────────
plugins: {
markdownShortcuts: true,
inlineMarkdown: true, // **bold**, *italic*, `code`, ~~strike~~ as you type
smartTypography: true, // -- → —, (c) → ©, curly quotes, linkify on paste
findReplace: true,
dragReorder: true,
formatPainter: true,
emoji: true, // :shortcode: autocomplete
},
// …toolbar, bubbleMenu, slashMenu, editor, history (below)
};
export default config;
marks: { bold: true, italic: true, link: true }, // only these three
plugins: { markdownShortcuts: true }, // findReplace/drag/painter off
Editor behaviour & history #
editor: {
placeholder: "Write something, or type '/' for commands…",
spellcheck: true,
autofocus: false,
readOnly: false,
attribution: true, // small "Made with Rune" credit; false removes it
// Optional image-upload hook — avoids base64 bloat.
// Receives a File, must return Promise<string> (the hosted URL).
// uploadImage: (file) => fetch('/api/upload', { method:'POST', body: form })
// .then(r => r.json()).then(d => d.url),
},
history: {
enabled: true,
maxSteps: 100, // undo stack depth
},
| Option | Default | What it does |
|---|---|---|
placeholder | 'Write something…' | Empty-state text. |
spellcheck | true | Toggles the browser spellchecker. |
autofocus | false | Focus the editor on mount. |
readOnly | false | Render content but block editing. |
attribution | true | Shows a small “Made with Rune” credit linking to parityfox.com. Set false to remove. |
uploadImage | — | Async hook: (file) => Promise<url>. Without it, images embed as base64. |
The slash menu #
Type / at the start of an empty line (or after a space) to open the block inserter. Keep typing to filter; the menu lists only enabled blocks.
- ↑ / ↓ — move the selection
- Enter — insert the highlighted block
- Esc — close the menu
Available commands: /Paragraph, /Bullet List, /Numbered List, /Quote, /Code Block, /Divider, /Callout, /Task List, /Video, /Image, /Table, /Toggle, /Columns. See the full catalogue on the Features page.
Keyboard shortcuts #
macOS shown; on Windows/Linux use Ctrl for ⌘ and Alt for ⌥.
| Shortcut | Action |
|---|---|
| ⌘B | Bold |
| ⌘I | Italic |
| ⌘U | Underline |
| ⌘⇧S | Strikethrough |
| ⌘E | Inline code |
| ⌘K | Insert / edit link |
| ⌘⌥1 / 2 / 3 | Heading 1 / 2 / 3 |
| ⌘⇧8 | Bullet list |
| ⌘⇧7 | Numbered list |
| ⌘⇧B | Blockquote |
| ⌘⇧C | Code block |
| ⌘Z | Undo |
| ⌘⇧Z / ⌘Y | Redo |
| ⌘F | Find & Replace |
| / | Slash command menu |
| Tab | Indent list item · next table cell · indent in code block |
Markdown shortcuts #
With the markdownShortcuts plugin on, type these at the start of a line. They’re block-level conversions, triggered on Space (or Enter for the divider).
| Type | Then press | Result |
|---|---|---|
# | Space | Heading 1 |
## | Space | Heading 2 |
### | Space | Heading 3 |
> | Space | Blockquote |
- or * | Space | Bullet list |
1. | Space | Numbered list |
``` | Space | Code block |
--- | Enter | Divider (horizontal rule) |
**bold** or `code` isn’t auto-converted — use ⌘B / ⌘E or the bubble menu instead. Shortcuts are suppressed inside code blocks, list items, callouts and task lists.Export #
editor.getHtml() // → sanitized HTML string
editor.getText() // → plain text
editor.getMarkdown() // → Markdown string
editor.getJSON() // → portable JSON document
editor.print() // → opens a clean print dialog
Markdown-looking text is converted automatically on paste (disable with pasteMarkdown: false).
Render without a DOM
Two helpers are exported standalone, so a stored document can be rendered to HTML on the server (Node) with no browser:
import { markdownToHtml, jsonToHtml } from '@parityfox/rune-editor';
markdownToHtml('# Hello') // → '<h1>Hello</h1>'
jsonToHtml(editor.getJSON())// → HTML string, no DOM required
Theming & dark mode #
Rune ships no opinions about how it looks. Every colour, radius and type token is a CSS custom property on :root — override what you like.
:root {
--rune-color-bg: #ffffff;
--rune-color-fg: #1a1a1a;
--rune-color-muted: #9b9b9b;
--rune-color-border: #e9e9e7;
--rune-color-accent: #2383e2;
--rune-color-surface: #f7f7f5;
--rune-color-hover: #f1f1ef;
--rune-color-active-bg: #e8f0fc;
--rune-color-active-fg: #2383e2;
--rune-font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
--rune-font-mono: 'JetBrains Mono', ui-monospace, monospace;
--rune-font-size: 16px;
--rune-line-height: 1.75;
--rune-radius: 6px;
--rune-radius-lg: 10px;
}
Dark mode — add data-theme="dark" to <html> or any ancestor:
document.documentElement.dataset.theme = 'dark';
JavaScript API #
Everything createFromConfig returns is a full Editor instance.
Content
editor.getHtml() // → HTML string
editor.setHtml('<p>…</p>') // set content (sanitized)
editor.getText() // → plain text
editor.getMarkdown() // → Markdown
editor.setMarkdown(md) // ← replace content from Markdown
editor.insertMarkdown(md) // ← insert Markdown at the caret
editor.getJSON() // → portable JSON document
editor.setJSON(doc) // ← replace content from JSON
editor.isEmpty() // → boolean
Commands & chaining
Run any registered command by name, or queue several and run them together. The chain stops if a command returns false.
editor.cmd('toggleBold')
editor.cmd('setTextColor', '#e03e3e')
editor.cmd('setHeading', 2)
editor.cmd('insertBlock', 'callout')
// chainable
editor.chain().toggleBold().toggleItalic().run()
Common commands: toggleBold, toggleItalic, toggleUnderline, toggleStrike, toggleInlineCode, toggleLink / setLink(href,text) / unsetLink(), setHeading(level), toggleBulletList, toggleOrderedList, toggleBlockquote, toggleCodeBlock, insertHorizontalRule, insertCallout(emoji,color), insertTaskList, insertTable(rows,cols), insertImage(src,alt,caption), insertVideo(url), setFontSize, setFontFamily, setTextColor, setTextBackground, setTextAlign, setLineHeight, indentBlock, outdentBlock, clearFormat, undo, redo.
State
editor.focus()
editor.blur()
editor.enable()
editor.disable()
editor.isActive('bold') // → boolean (mark active / block current)
editor.destroy()
Events #
Subscribe through the editor’s event bus.
editor.events.on('change', ({ editor, html }) => { … })
editor.events.on('selectionchange', ({ editor }) => { … })
editor.events.on('keydown', ({ editor, event }) => { … })
editor.events.on('paste', ({ editor }) => { … })
editor.events.on('slash:open', ({ editor }) => { … })
editor.events.on('destroy', () => { … })
| Event | Fires when | Payload |
|---|---|---|
change | Content changes (input, setHtml, undo/redo) | { editor, html } |
selectionchange | The selection moves | { editor } |
keydown | A key is pressed (after built-in shortcuts) | { editor, event } |
paste | After paste is sanitized + inserted | { editor } |
slash:open | The slash menu opens | { editor } |
destroy | destroy() is called | — |
The bus supports on, once, off, and emit.
Writing extensions #
Blocks, marks, formatting and plugins are all plain objects with a name and type. Hand Rune an object — no class hierarchy, no schema DSL. Pass custom extensions via the Editor’s extensions array.
Block extension
export const MyBlock = {
name: 'myBlock',
type: 'block',
tag: 'div',
match: (el) => el.dataset?.type === 'myBlock',
commands(editor) {
return {
insertMyBlock: () => editor.cmd('insertBlock', 'myBlock'),
};
},
slashItem: {
icon: '▦', title: 'My Block', description: 'Insert a custom block',
action: (editor) => editor.cmd('insertMyBlock'),
},
toolbarItem: {
name: 'myBlock', icon: '<svg>…</svg>', title: 'My Block',
action: 'insertMyBlock', isActive: (editor) => editor.isActive('myBlock'),
},
};
Mark extension
export const MyMark = {
name: 'myMark',
type: 'mark',
tag: 'span',
execCommand: 'underline', // auto-registers toggleMyMark + isActive
keymap: {
'Meta+m': (editor) => editor.cmd('toggleMyMark'),
'Control+m': (editor) => editor.cmd('toggleMyMark'),
},
toolbarItem: {
name: 'myMark', icon: '<svg>…</svg>', title: 'My Mark',
action: 'toggleMyMark', isActive: (editor) => editor.isActive('myMark'),
},
};
Plugin extension
export const MyPlugin = {
name: 'myPlugin',
type: 'plugin',
init(editor) {
editor.content.addEventListener('keydown', (e) => {
// attach behaviours, listen to editor.events, etc.
});
},
commands(editor) {
return { myCommand: () => { /* … */ } };
},
};
name, type (block/mark/formatting/plugin), tag, match(el) (blocks), execCommand + toggleCommand (marks), commands(editor), keymap, toolbarItem, slashItem, and init(editor) (plugins).Real-time collaboration #
Collaboration is opt-in and lives in collab/. The core editor stays dependency-free; the collab layer’s deps (Yjs, ws, y-websocket, y-indexeddb) are dev/server-only. Bind a Yjs document and presence to an editor instance:
import * as Y from 'yjs';
import { WebSocketProvider } from '@parityfox/rune-editor/collab/provider.js';
import { bindParagraphSpike } from '@parityfox/rune-editor/collab/paragraph-binding.js';
import { bindPresence } from '@parityfox/rune-editor/collab/presence.js';
const doc = new Y.Doc();
const provider = new WebSocketProvider('ws://localhost:1234', 'my-doc', doc);
bindParagraphSpike(editor, doc); // sync the document
bindPresence(editor, doc, provider.awareness, { // live cursors
name: 'Alice', color: '#2563eb',
});
Run the reference sync server with npm run collab-server, then open the two-pane demo at examples/collab. There’s also an in-process memory-hub transport for demos and tests — no server needed.
docs/:
- Collaboration overview — architecture, document model, every feature, limitations
- API reference — every
collab/module with signatures - Server & deployment — the reference server, the
authorize()auth hook, persistence, production
See it all in one place.
Browse the complete catalogue of blocks, marks, and plugins.
Feature reference