# Styled Source Editor — Design Plan ## Core Concept The editor is always a markdown text editor. There is no separate "WYSIWYG mode" — the user edits markdown directly, but the editor applies CSS styling that makes it look like rendered output. Delimiters (`**`, `*`, `` ` ``, etc.) are hidden when the cursor is outside the element and revealed when the cursor enters it. ## Two CSS States (not modes) - **Editing**: `contentEditable="true"`, delimiters revealed on cursor focus - **Viewing**: `contentEditable="false"`, all delimiters hidden No content transformation on state switch. The DOM is identical in both states — only CSS changes. This eliminates all conversion-during-editing bugs. ## DOM Structure The editor contains markdown text wrapped in styled spans: ```html
## Hello World
Some **bold** and *italic* text.
- First item
> Quoted text
``` CSS handles all visual rendering: ```css .md-delim { display: none; color: #999; font-weight: normal; } .md-bold.editing .md-delim, .md-italic.editing .md-delim { display: inline; } .md-bold { font-weight: bold; } .md-italic { font-style: italic; } .md-heading[data-level="1"] { font-size: 2em; font-weight: bold; } .md-list-item { display: list-item; margin-left: 1.5em; } .md-blockquote { border-left: 3px solid #ccc; padding-left: 1em; } .md-code { font-family: monospace; background: #f5f5f5; } ``` ## Per-Keystroke Pipeline 1. User types a character → browser inserts it into the DOM (contentEditable) 2. `input` event fires 3. Parser scans the **current line only** (the block element containing the cursor) 4. If the span structure needs updating (e.g. user just typed the closing `**`): - Wrap/unwrap the affected text range using targeted DOM operations - No innerHTML rebuild, no full-document re-parse 5. If a block pattern is detected (e.g. `# ` at start of line): - Update the block element's class and data attributes - Move the delimiter text into a `.md-delim` span ## Key Operations ### Inline formatting detection When the user types a delimiter character, scan backward in the current text node for a matching opener. If found, wrap the range: ``` Before: hello **world** After: hello **world** ``` Use `Range` and `surroundContents` for the wrap — no innerHTML. ### Block detection When the user types a space after `#`, `>`, `-`, `1.`, etc. at the start of a line, update the block element: ``` Before:
# Title
After:
# Title
``` ### Cursor focus tracking On `selectionchange`, find the nearest formatting span and add an `.editing` class so CSS reveals its delimiters. Remove `.editing` from the previous span. ## getMarkdown() Read `textContent` from the editor element. The delimiter spans contain the actual delimiter characters, so `textContent` produces valid markdown. No conversion needed. ## getHTML() Run the existing tokenizer + `toHTML` pipeline on the markdown string from `getMarkdown()`. This is only called on demand (export, save, API), never during editing. ## Macros Macros are rendered as `contentEditable="false"` islands within the editable text. The macro source (`@user`) is stored in a `data-source` attribute. The rendered output is displayed inside the island. On focus, the island could expand to show the source for editing. For `toMarkdown`, macro islands emit their `data-source` value. ## Initial Load Markdown → styled source DOM is a one-time conversion on editor init: 1. Parse markdown using the existing tokenizer (produces token stream) 2. Walk the token stream, creating the span structure described above 3. Set the editor's innerHTML once This replaces the current `toHTML` → innerHTML path. ## What This Eliminates - `transformInline` and its innerHTML rebuild - `blockToMarkdown` / `nodeToMarkdown` (DOM → markdown string → DOM) - The flatten-rebuild pipeline and all its escaping bugs - The `
` + ZWS cursor anchor workarounds - The sentinel marker system for preserved HTML elements - Mode switch conversions (WYSIWYG ↔ view ↔ edit) ## What This Keeps - The tokenizer (for initial load and `getHTML()`) - The serializer (for `getHTML()` via `toMarkdown` → `toHTML`) - Tag definitions (for block pattern matching and toolbar buttons) - The `BaseTag` keyboard dispatch system - The collaboration transport layer - The macro system ## Implementation Order 1. Build the markdown → styled DOM renderer (replaces `toHTML` for editor init) 2. Build the per-line parser that updates span structure on keystroke 3. Build the inline delimiter detection (wrap/unwrap via Range) 4. Wire up cursor focus tracking for delimiter reveal 5. Implement `getMarkdown()` as `textContent` read 6. Remove `transformInline`, `blockToMarkdown`, and the rebuild pipeline 7. Update tests ## Branch Work on the `styled-source` branch, branched from current `main`.