<writemark-editor> is a dependency-free live inline Markdown editor web component.
The default experience is inline/live editing: headings, inline formatting, lists, task checkboxes, code fences, and tables render inside the editor itself while the component preserves raw Markdown as the canonical value and form submission value. There is no built-in formatting toolbar; formatting is driven by Markdown shortcuts, slash commands, keyboard shortcuts, and the public action API.
100% vibe-coded. Writemark was built entirely by directing coding agents rather than manually writing the implementation. The result is still held to ordinary software constraints: zero runtime dependencies, canonical Markdown as the source of truth, generated-build verification, package dry runs, and a cross-browser Playwright suite.
Try the live demo · Read the documentation
npm install writemark-editorImporting the package registers <writemark-editor>:
import 'writemark-editor';<writemark-editor name="body" label="Body"></writemark-editor>Writemark explores a specific tradeoff: make Markdown feel rendered while it is being edited without replacing the Markdown with an opaque document model. Raw Markdown remains the value that forms submit and application code reads. The component owns the editing mechanics; the host application keeps control of its toolbar, persistence, uploads, and product UI.
Its dependency-free renderer supports a practical CommonMark-inspired subset
with optional GFM-style features. The commonmark and gfm flavor values are
feature profiles, not claims of complete specification conformance.
Choose the level that matches what you are building:
| Level | Guide | Use it for |
|---|---|---|
| 1 | Getting started | Install the component, create an editor, read its value, and submit it in a form. |
| 2 | Features and editing | Learn Markdown support, keyboard behavior, slash commands, tables, code blocks, selection, and clipboard handling. |
| 3 | Advanced integration | Build host controls, custom actions, completion providers, file workflows, validation, and production integrations. |
| Reference | API reference | Look up attributes, properties, methods, actions, events, CSS variables, parts, and exports. |
See the documentation index for suggested learning paths and the core concepts used throughout the guides.
The public demo runs entirely in the browser and does not require an account or installation.
To run the same demo locally, open demo/index.html directly in a browser or
serve it from the repository:
cd writemark-editor-v1
npm startThen open:
http://127.0.0.1:5173/demo/index.html
To test from an iPhone on the same Wi-Fi network, expose the development server on the computer's network interfaces:
npm run start:lanStartup prints the complete local and phone-ready paths, using each external IPv4 address detected on the computer:
Local: http://127.0.0.1:5173/demo/index.html
Network (en0): http://192.168.1.25:5173/demo/index.html
Open the Network URL on the iPhone. The equivalent explicit option is
npm start -- --host=0.0.0.0; npm run serve -- --lan also enables LAN
access without opening a desktop browser. Keep LAN mode to trusted networks,
because it serves files from this checkout to other devices on the network.
No npm install is required. The dev server uses Node's built-in HTTP module and is only needed for module-based test and performance pages.
Maintainers can find the one-time GitHub Pages setup and publication checks in Releasing Writemark.
src/ Canonical editor source.
dist/ Generated package/browser files.
docs/ Layered usage, feature, integration, and API guides.
demo/ Direct-open browser demo.
tests/ Isolated Playwright specs, fixture, support code, and test guide.
perf/ Performance harness.
scripts/ Build and local server utilities.
For direct browser usage without a module server:
<script src="./dist/writemark-editor.global.js"></script>
<writemark-editor
name="body"
label="Body"
mode="live"
placeholder="Type / for commands"
></writemark-editor>For an npm package loaded by a bundler:
import 'writemark-editor';<writemark-editor
name="body"
label="Body"
mode="live"
placeholder="Type / for commands"
></writemark-editor>dist/ is generated from src/writemark-editor.js with npm run build; do not edit generated files directly.
Compatibility: md-live-editor.js and <md-live-editor> are still registered as legacy aliases for existing demos or consumers.
<writemark-editor mode="live"></writemark-editor>
<writemark-editor mode="source"></writemark-editor>
<writemark-editor mode="split"></writemark-editor>
<writemark-editor mode="preview"></writemark-editor>| Mode | Behavior |
|---|---|
live |
Default. Rendered inline editor. Markdown remains canonical. |
source |
Raw Markdown textarea fallback. |
split |
Source textarea plus rendered preview. |
preview |
Read-only rendered preview. |
Live mode is experimental and unsupported on mobile browsers. It has not been
certified for native selection, software-keyboard boundary editing, autocorrect,
or IME composition on iOS, iPadOS, Android, or other mobile environments. Use
mode="source" for production mobile editing:
<writemark-editor mode="source"></writemark-editor>The host application should choose this mode explicitly; Writemark does not switch modes by touch detection. A tablet hardware keyboard does not change the mobile support status.
Inline/live editing is the primary workflow. Use mode="source" when every
Markdown marker must be directly visible, mode="split" for source beside a
rendered preview, or the preview setting to add an optional preview to an
editing mode.
Code fences are refined in live mode: the opening and closing backtick markers remain in canonical Markdown source, but they do not appear inside the rendered editable code block. Use source mode to edit the fence markers directly.
| Input | Behavior |
|---|---|
| Enter in nonempty list item | Creates next item at same level. |
| Enter in empty list item | Exits the list. |
| Tab in list item | Indents the item. |
| Shift+Tab in list item | Outdents the item. |
Enter after ```python |
Creates a closed code fence and places cursor inside. |
/ at line start |
Opens slash command menu. |
```py |
Opens code-language completion; py ranks Python. |
| Enter in table cell | Inserts a row below the current row and moves into the first new cell. |
| Shift+Enter or Escape in table cell | Exits the table to a blank line after it. |
| Tab in table cell | Moves to the next cell; from the last nonempty row, creates a new row. |
| Shift+Tab from first table cell | Exits before the table. |
| Cmd/Ctrl+A in live mode | Expands selection progressively: cell/block → row/table/section → document. |
| Delete/Backspace after expanded selection | Deletes the selected canonical Markdown range, including full-document selections. |
| Backspace at start of line | Joins the current logical Markdown line with the previous line and places the caret at the join point. |
| Delete at end of line | Joins with the next logical Markdown line. |
#, -, 1., >, or [ ] then Space |
Applies Markdown shortcut behavior. |
| Pasted tab-separated data | Converts to a Markdown table. |
| Pasted HTML without plain text | Converts basic HTML to Markdown. |
/table or editor.exec('block.table', { rows: 1, cols: 3 }) inserts a valid Markdown table:
| Column 1 | Column 2 | Column 3 |
| --- | --- | --- |
| Cell 1 | Cell 2 | Cell 3 |In live mode, the table is rendered as an editable grid. The delimiter row is preserved in the Markdown source but hidden from the primary grid UI. Enter creates a row below the current row. Shift+Enter or Escape exits the table to a blank line after it. Tab moves across cells; when it reaches the last nonempty row, it creates a new row, and from an empty terminal row it exits the table.
Table row/column mutation is exposed through actions, not rendered buttons: table.insertRowAfter, table.insertColumnAfter, table.deleteRow, and table.deleteColumn.
Code blocks render without leaking fence markers into the editable code content. The compact header shows the language label only. Use slash commands, Markdown syntax, or editor.exec('code.setLanguage', { language: 'python' }) to change the canonical opening fence.
Live mode routes copy, cut, and paste through the canonical Markdown source instead of trusting browser contenteditable mutations.
Copy/cut behavior:
- Copies selected source-backed Markdown to
text/plain,text/markdown, andtext/x-markdown. - Also writes sanitized rendered HTML to
text/htmlfor rich destinations. - Exact inline selections expand to include Markdown formatting delimiters. Selecting the visible
boldtext inside**bold**copies**bold**; selecting a link label copies[label](url). - Full-document and expanded selections preserve hidden Markdown source such as table delimiter rows and code fences.
Paste behavior:
- Prefers explicit Markdown clipboard data when available.
- Preserves pasted Markdown as Markdown and renders it immediately in live mode.
- Converts tab-separated spreadsheet data to a Markdown table.
- Converts rich HTML to Markdown when the paste source does not already provide Markdown-like plain text.
- Emits
md-pasteafter canonical Markdown insertion. - File paste/drop still emits host-controlled file events; the component does not upload files.
const editor = document.querySelector('writemark-editor');
editor.value = '# Hello';
editor.mode = 'live';
editor.exec('block.heading.2');
console.log(editor.value); // Markdown
console.log(editor.getMarkdown()); // Markdown
console.log(editor.getHTML()); // Sanitized rendered HTML
console.log(editor.getText()); // Plain text
console.log(editor.getPlainText());| Property | Type |
|---|---|
value |
string raw Markdown |
defaultValue |
string reset value |
mode |
`"live" |
preview |
`"none" |
tabBehavior |
`"accessibility-first" |
indentString |
`"\t" |
debug |
nonnegative diagnostic level; default 0 |
debugLog |
optional console.debug mirror |
selectionStart / selectionEnd |
source offsets |
dirty |
boolean |
| Method | Description |
|---|---|
focus() |
Focus the active editor surface. |
select() |
Select the full Markdown value. |
exec('editor.selectAllExpand') |
Expand source-backed live selection progressively. |
exec('editor.deleteSelection') |
Delete the current source-backed selection. |
setSelectionRange(start, end, direction?) |
Set source-backed selection. |
exec(actionId, args?) |
Execute an action. |
registerAction(action) |
Add custom action. |
registerCompletionProvider(provider) |
Add custom completion provider. |
getMarkdown() / setMarkdown(markdown) |
Read or write raw Markdown. |
insertMarkdown(markdown) |
Insert Markdown at the current source-backed selection. |
getSelectionMarkdown() |
Return selected source Markdown. |
getHTML() |
Return sanitized rendered HTML. |
getText() / getPlainText() |
Return plain text. |
getCurrentBlock() / getSelectedBlocks() |
Inspect parsed source-backed blocks. |
getActiveMarks() |
Return active formatting/action IDs for custom host UI. |
canExec(actionId, args?) |
Check whether an action is currently available. |
find(query, options?) |
Find and select source text. |
replace(query, replacement, options?) |
Replace the current/next match. |
replaceAll(query, replacement, options?) |
Replace all matches. |
commit() |
Mark current value as clean and emit md-change. |
reset() |
Restore default value. |
Examples:
editor.exec('block.heading.1');
editor.exec('block.table', { rows: 1, cols: 3 });
editor.exec('block.codeFence', { language: 'python' });
editor.exec('inline.bold');
editor.exec('inline.link', { url: 'https://example.com' });Important action IDs:
editor.smartEntereditor.smartTabeditor.smartOutdenteditor.smartBackspaceeditor.smartDeleteeditor.markdownShortcuteditor.deleteSelectioneditor.selectAllExpandblock.heading.1throughblock.heading.6block.bulletListblock.orderedListblock.taskListblock.blockquoteblock.codeFenceblock.tabletable.insertRowAftertable.insertColumnAftertable.deleteRowtable.deleteColumncode.setLanguageinline.boldinline.italicinline.codeinline.linkinline.imageview.liveview.source
| Event | Description |
|---|---|
md-before-change |
Cancelable event fired before a transaction mutates the canonical Markdown. |
md-input |
Value changed. |
md-change |
Committed value change. |
md-selection-change |
Source-backed selection changed. |
md-action |
Action executed. |
md-completion-open |
Completion popup opened. |
md-completion-close |
Completion popup closed. |
md-completion-accept |
Completion accepted. |
md-render |
Preview/rendered HTML generated. |
md-file-paste |
File pasted. Host decides upload/insertion. |
md-file-drop |
File dropped. Host decides upload/insertion. |
md-copy |
Live-mode copy wrote Markdown clipboard data. |
md-cut |
Live-mode cut wrote Markdown clipboard data and removed the canonical range. |
md-paste |
Clipboard content was inserted through the canonical Markdown path. |
md-dirty-change |
Dirty state changed. |
md-debug |
Opt-in, serializable input/selection diagnostic event controlled by debug. |
md-error |
Recoverable editor error. |
Debug output is event-first and disabled by default:
editor.debug = 2;
editor.addEventListener('md-debug', event => {
diagnostics.push(event.detail);
});
// Optional; events are still emitted.
editor.debugLog = true;Level 1 reports input decisions and source-backed deletion. Level 2 adds
selection and focus restoration details. Payloads contain source offsets and
lengths, not the Markdown body. Selection events can identify whether offsets
came from a direct selection or a composed shadow-DOM range, which is useful
when diagnosing Safari. Writemark does not store diagnostics; the host decides
whether to display, retain, or transmit each md-debug event. The published
demo buffers enabled debug events until Clear log and provides a Copy
debug info button, including a fallback for iOS Safari on the HTTP LAN test
URL.
<form>
<writemark-editor name="body" label="Body" required></writemark-editor>
<button>Submit</button>
</form>The submitted value is raw Markdown.
Horizontal rules now render as a clean divider in live mode. The raw Markdown marker stays in the canonical source value and is visible in mode="source", but live mode no longer leaks ---, --, or _ _ _ text next to the divider.
The component exposes CSS parts and variables.
writemark-editor::part(live-editor) {
min-height: 400px;
}
writemark-editor {
--md-editor-font: Inter, system-ui, sans-serif;
--md-editor-mono-font: "JetBrains Mono", monospace;
--md-editor-min-height: 360px;
}The active line/table-cell blue inset outline is disabled by default. To keep it removed, no CSS override is required. To add a subtle non-blue active block affordance:
writemark-editor {
--md-editor-active-line-bg: color-mix(in srgb, CanvasText 4%, transparent);
--md-editor-active-cell-bg: color-mix(in srgb, CanvasText 4%, transparent);
}To re-enable an outline explicitly:
writemark-editor {
--md-editor-active-line-ring: inset 0 0 0 2px color-mix(in srgb, Highlight 35%, transparent);
--md-editor-active-cell-ring: var(--md-editor-active-line-ring);
}Common parts:
containerlabeleditorlive-editortextareapreviewcompletion-popupcompletion-itemcompletion-item-activetabletable-cellcode-blockcheckboxstatuserror
Common focus/active styling variables:
--md-editor-focus-ring: outer editor focus ring.--md-editor-border-focus: outer editor focused border color.--md-editor-active-line-ring: focused live block ring. Defaults tonone.--md-editor-active-line-bg: focused live block background. Defaults totransparent.--md-editor-active-cell-ring: focused table-cell ring. Defaults to--md-editor-active-line-ring.--md-editor-active-cell-bg,--md-editor-transition-duration,--md-editor-transition-ease: focused table-cell background. Defaults to--md-editor-active-line-bg.
The renderer is safe by construction for built-in Markdown output:
- Raw HTML is not executed.
- User text is escaped.
- Dangerous URL schemes such as
javascript:are blocked. - File paste/drop emits events; the component does not upload anything.
- No network calls are made by default.
Still validate and sanitize server-side when storing or rendering user-generated content outside this component.
The browser suite exercises this boundary with a reviewed hostile-input corpus, seeded and shrinkable property tests over arbitrary Markdown, source-range and determinism invariants, and differential tests against a pinned CommonMark reference renderer over the supported subset. Run the focused security suite with:
npm run test:fuzznpm run check
npm testnpm test verifies generated files and documentation, starts the local test
server, and runs every Playwright project supported on the current host. The
Linux CI gate always runs Chromium, Firefox, and WebKit. It also runs the
complete input contract in an iPhone 13 Mobile Safari context. Playwright no
longer provides a current WebKit build for macOS 14 and older, so the local
config omits those unavailable WebKit projects on those hosts.
The current desktop suite registers 465 independent cases per browser project, including every one of the 230 checks migrated from the retired page-hosted suite. It also includes a browser input and synthetic IME-composition contract, a hostile Markdown corpus, property-based parser and sanitizer checks, and CommonMark differential coverage. Playwright owns discovery, isolation, browser lifecycle, and assertions; the specs drive real keyboard, pointer, form, and host-API workflows. Failed expectations, uncaught page errors, and browser console errors make the command exit nonzero.
For a faster Chromium-only development pass:
npm run test:browser:chromiumFor an interactive Chromium run:
npm run test:browser:headedThe Playwright tests are organized by component contract, state and forms, editing, completion, Markdown rendering, security, clipboard, navigation, tables, performance, and the published demo. See tests/README.md for the suite structure, fuzz replay, and debugging workflow.
See RELEASING.md for the one-time first npm publish and the automated GitHub Release workflow used for later versions.
This version implements source-backed live inline editing without third-party runtime dependencies. Development tooling includes Playwright coverage in desktop Chromium, Firefox, and WebKit. Live mode is experimental and unsupported on mobile; use source mode for production mobile editing. Production certification also requires screen-reader verification, IME testing, high-contrast review, target-device testing, and an independent security review.
