From 818ee418d5f7f6a25ad50583045179a87cb1b958 Mon Sep 17 00:00:00 2001 From: gsb Date: Thu, 30 Apr 2026 22:49:09 +0000 Subject: [PATCH] docs: Styled source editor design plan Design document for replacing the WYSIWYG innerHTML rebuild approach with a styled-source model where the editor always contains markdown text with CSS styling. No mode conversions during editing, no innerHTML rebuild, no round-trip bugs. --- STYLED_SOURCE_DESIGN.md | 161 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 STYLED_SOURCE_DESIGN.md diff --git a/STYLED_SOURCE_DESIGN.md b/STYLED_SOURCE_DESIGN.md new file mode 100644 index 0000000..00c783c --- /dev/null +++ b/STYLED_SOURCE_DESIGN.md @@ -0,0 +1,161 @@ +# 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`.