Skip to content

Commit 7bc6cce

Browse files
committed
3.3.2
1 parent c6b73d1 commit 7bc6cce

52 files changed

Lines changed: 1455 additions & 266 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Backspace/Delete — One Decision Model
2+
3+
Rules for `src/core/event/rules/keydown.rule.{backspace,delete}.js`. The two are mirrors (`front`/`end`).
4+
Nearly every bug found here came from one habit: a branch deciding "where am I?" from **local siblings**,
5+
so a list or quote boundary looked like the end of the document.
6+
7+
## The model — ask these, in this order
8+
9+
1. **Is the caret at the line's edge?**`format.isEdgeLine(...)`, or `isEdgeBreakCaret(...)` when the
10+
caret sits on an empty line's `<br>`.
11+
2. **What is the neighbouring line?**`getAdjacentLine(format, formatEl, edge)` — the previous/next line
12+
**in document order**, stepping out of and into blocks *and out of nested list cells*, stopping at
13+
closure blocks (table cell) and the root. It returns `null` both at the document edge and when the
14+
neighbour is not a line (a component) — when the two must be told apart, use `getAdjacentElement`
15+
(same walk, unfiltered).
16+
3. Then exactly one of:
17+
- **No neighbour at all** (`getAdjacentElement` is `null`) → document edge. `preventStop` so the
18+
browser can't reach outside, do nothing else. A component next door is **not** the document edge —
19+
fall through so the component-select branches can claim it; preventing there kills the key AND the
20+
component selection.
21+
- **This line is empty** → drop it, caret to the neighbour (`getEmptyLineMergeTarget` + the
22+
`emptyLine.merge*` effects). Same path for a plain line and a list cell.
23+
- **Neighbour is across a block boundary** (`neighbor.parentElement !== formatEl.parentElement`) →
24+
merge it ourselves (`mergeLineInto`). The browser merges these badly.
25+
- **Plain sibling lines** → return `true` without preventing. Native merge is correct here; don't
26+
reimplement it.
27+
28+
List cells are ordinary lines in this model (`isNormalLine('LI')` is `true`). Two deliberate exceptions,
29+
both because a list owns a *different* gesture there:
30+
- Backspace at the **head of a cell** outdents — the list branch owns it, so the merge branch skips cells.
31+
- A cell holding a nested list belongs to `getNestedListTarget`, which lifts the list instead.
32+
33+
## Never navigate by local siblings
34+
35+
`formatEl.nextSibling` / `previousElementSibling` answer "within my parent", not "in the document". Using
36+
them for edge decisions is what made Delete dead at the last `<li>` (last in the `<ul>` read as end of
37+
document) and at a line before a list. **Use `getAdjacentLine`.** Raw sibling access is fine only for
38+
inspecting the line's own children.
39+
40+
## The caret's neighbours may be invisible
41+
42+
`<br>` and zero-width text are filler, not content — a raw sibling test sees them and reports "not at the
43+
edge", so the rule stands down and the browser just eats the filler: one keypress, nothing visible.
44+
Producers clean up (`_normalizeEditRange` drops the zero-width once the caret moves onto the `<br>`;
45+
`backspace.list.mergePrev` calls `stripTrailingBreaks`), and helpers skip filler when walking siblings.
46+
47+
## Fail open — a prevented key must do something
48+
49+
`contentEditable` hands us shapes we did not predict; the failure that hurts is the key doing **nothing**.
50+
51+
- Never `preventDefault` on a path that then makes no DOM change, no caret move, no component selection.
52+
The only deliberate no-ops: document start (Backspace), document end (Delete), closure-block boundaries.
53+
- **Never report a key as handled when the branch had nothing to do** — that stops every rule behind it.
54+
Gate entry on the target existing (`getNestedListTarget(...)` before the nested-list branch) — but gate
55+
only the path that needs it: a collapsed caret needs a nested list to lift, while a real selection is
56+
handled (`html.remove`) regardless, so the gate must not cut that path off.
57+
- No branch applies → return `true` without preventing, and let the browser do it.
58+
59+
## Verify before claiming it works
60+
61+
jsdom cannot perform native contentEditable edits, so `changed === false` only means *the editor* did not
62+
act. Judge by emitted actions, and sweep:
63+
64+
- Dump the reduced action list (`reduceDeleteDown(actions, ports, ctx)`) — that is the real decision.
65+
- Sweep a document set × {Backspace, Delete} × {front, end} over every line, flagging
66+
`prevented && DOM unchanged && caret unchanged`, any throw, and any lost table cell.
67+
- Always check all four directions of a structure pair (p→list, list→p, and both Backspace/Delete) and the
68+
**paragraph equivalent**. A divergence between them is the bug.
File renamed without changes.
File renamed without changes.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: 수동 코드 수정 후 changes.md에 누락된 변경사항 추
44
---
55

66
The user has manually modified code and needs `changes.md` updated.
7-
(When Claude modifies code, `changes.md` is updated automatically per CLAUDE.md instructions. This skill is only for user-initiated changes.)
7+
(When an AI agent (Claude, Codex, ...) modifies code, `changes.md` is updated automatically per its instructions (CLAUDE.md / AGENTS.md). This skill is only for user-initiated changes.)
88

99
Follow the rules defined in `prompts/changes-guide.md` exactly.
1010

File renamed without changes.

0 commit comments

Comments
 (0)