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.
This commit is contained in:
parent
bfc20f56bf
commit
818ee418d5
161
STYLED_SOURCE_DESIGN.md
Normal file
161
STYLED_SOURCE_DESIGN.md
Normal file
|
|
@ -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
|
||||
<div id="ribbit">
|
||||
<div class="md-heading" data-level="2">
|
||||
<span class="md-delim">## </span>Hello World
|
||||
</div>
|
||||
<div class="md-paragraph">
|
||||
Some <span class="md-bold">
|
||||
<span class="md-delim">**</span>bold<span class="md-delim">**</span>
|
||||
</span> and <span class="md-italic">
|
||||
<span class="md-delim">*</span>italic<span class="md-delim">*</span>
|
||||
</span> text.
|
||||
</div>
|
||||
<div class="md-list-item">
|
||||
<span class="md-delim">- </span>First item
|
||||
</div>
|
||||
<div class="md-blockquote">
|
||||
<span class="md-delim">> </span>Quoted text
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
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: <span class="md-paragraph">hello **world**</span>
|
||||
After: <span class="md-paragraph">hello <span class="md-bold">
|
||||
<span class="md-delim">**</span>world<span class="md-delim">**</span>
|
||||
</span></span>
|
||||
```
|
||||
|
||||
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: <div class="md-paragraph"># Title</div>
|
||||
After: <div class="md-heading" data-level="1">
|
||||
<span class="md-delim"># </span>Title
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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 `<br>` + 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`.
|
||||
Loading…
Reference in New Issue
Block a user