Markdown editor
readme.md

Core #

The Zig editor engine owns document content, editing semantics, selection and undo history. It runs as WebAssembly inside the renderer, keeping interactive edits local to that process.

Edits must preserve document and history consistency on failure. Persistent text storage allows document versions to share unchanged content rather than copying the entire document for undo.

The core is independent of the DOM, desktop shell and filesystem. Browser input and visual selection mapping belong to the renderer; privileged file access belongs to Electron.

Cursor fixtures #

editor_fixture_test.zig describes editing cases with Markdown source, actions, and expected source:

try expect_keys("~~textˇ~~", &.{.enter}, "~~text~~\n\nˇ");
try expect_keys("**«text»**", &.{.delete_selection}, "ˇ");
try expect_keys("**textˇ**", &.{ .escape, .{ .text = "x" } }, "**text**xˇ");

ˇ marks the cursor. « marks the selection anchor and » its moving end; reversing their order tests a backward selection. Markers refer to source positions, including positions beside hidden formatting delimiters. They must be within a block body, not its heading, list, or fence prefix.

Fixtures check both exact source and exact selection. This catches damaged Markdown even when the visible text looks correct. Typing is applied one Unicode scalar at a time. boundary_left and boundary_right change sides of hidden delimiters; they do not simulate native arrow movement. These tests complement, rather than replace, browser tests for navigation, grapheme handling, and composition.

Inline projection contract #

Block and inline Markdown syntax are interpreted in the core. Inline runs describe source ranges and formatting; they do not replace the original Markdown or become a separate editable model.

Run ranges use UTF-8 byte offsets within a block body. The browser adapter converts them to UTF-16 offsets for DOM selection. Empty runs represent source positions on either side of hidden delimiters; they must not be discarded merely because they have no visible text.

The WASM bridge exposes a borrowed run buffer containing four u32 fields per run: source start, source end, formatting flags, and code delimiter width. The buffer remains valid until the next inline preparation or initialization; the adapter consumes it before making another allocating core call. Preparation failure clears the buffer and does not modify document content or history.

Inline boundary policy (proposed) #

Markdown defines how formatting is written, but not how the cursor should behave around it. Editors make their own choices. Paper should follow familiar editing habits:

  • Keep formatting while you continue typing. If you are writing in bold, italic, or strikethrough and reach the end of that text, new text keeps the same formatting. Turning formatting off makes the next text plain.
  • Make inline code easy to leave. You should be able to type before or after a code span without extending it. Leaving code should not add a space automatically. When it is unclear whether the cursor is inside or outside code, prefer outside.
  • Remember which side of the formatting the cursor is on. The end of bold text and the start of plain text can look like the same cursor position, but typing there can mean different things. Clicking, arrow keys, and replacing selected text should handle this consistently, including when styles are combined.
  • Make undo restore the writing context. Undo should restore the text, selection, and formatting that will apply when you resume typing. Pasting and typing through an input method should follow the same rules as ordinary typing.

The core and browser adapter implement boundary resolution, arrow-key exits, Escape at a span’s end, and same-block replacement and deletion over visible text. Exact source positions preserve whether typing is inside or outside a span, including through undo/redo. Dedicated formatting toggles and comprehensive nested-formatting, clipboard, and input-method validation are still future work. The roadmap tracks the remaining work.

References #

These sources inform the proposal; they do not establish a shared standard:

  • CommonMark defines Markdown syntax, not cursor behavior.
  • ProseMirror keeps formatting active at its end by default. Its stored marks record the formatting to use for the next input.
  • Typora shows and hides formatting markers as you edit, but does not document precise cursor rules.
  • Tiptap’s code extension provides an explicit way to leave inline code. Preferring plain text at an unclear code boundary is Paper’s choice.