hyper-undo 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # hyper-undo changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+ - `refused: 'skip'` scope option: one `undo()` or `redo()` press removes each entry that preflight or apply refuses (every code), emits a `skipped` event carrying the entry's plain-data description, and goes on to the next entry until one applies or the stack is empty. The receipt gains a `skipped` list, and the default (`null`) keeps a refused entry on top as before.
7
+ - `skipped` event: fired once per entry a `refused: 'skip'` press removes.
8
+ - `trackContent(root, { read, write, prop })`: a field's content is recorded as one guarded value step, its saved HTML before and after, sampled from the changes inside the root instead of the raw text and node steps those changes would otherwise leave. Returns a release function, and `stop()` forgets every tracked root.
9
+ - `durableForm` scope option: a host gives a node a stable saved form, used instead of its live markup when certifying and replaying steps
10
+ - `editableKeys: 'page'` scope option: in editable content, Cmd+Z, Cmd+Shift+Z, Ctrl+Y and the browser's own `beforeinput` `historyUndo`/`historyRedo` go to the page history instead of the browser's per-field undo. Form controls, IME compositions, the `shadowKeydownIn` editors and regions the scope does not record keep their own undo, and the default leaves every key with the browser.
11
+ - `ownKeysIn` option: selectors whose history keys belong to the page even inside a `shadowKeydownIn` editor.
12
+ - `holdReplay` option: a consumer can hold undo and redo, which then return `{ ok: false, code: 'held' }` without refusing the entry, touching a stack or changing the document.
13
+ - `typingBreak: 'sentence'` scope option: an undo press undoes one sentence of typing. A step closes after the space or line break that follows `.`, `?` or `!`, at a field switch, at a switch between typing and deleting, after a paste, a drop, a cut or a format command, when a selection is replaced, and at a caret jump made with the mouse or the navigation keys. An open IME composition never lets the idle timer close a step, on every scope.
14
+ - `endsSentence(textBefore)`: the sentence rule as a predicate, for a host that groups typing the same way.
15
+ - A successful `undo()` or `redo()` receipt gains `caret`, where the applied entry changed text: `{ node, offset }` for a text node or an added or removed one, and `{ root, textOffset }` for a tracked content root. The key is present only when the applied entry changed text; a refused press carries no `caret`. A press that skips entries and then applies one reports that entry's `caret`. The engine never touches the selection.
16
+
17
+ ### Fixed
18
+ - Changes inside a subtree removed from a `no-undo` region no longer leave a history entry
19
+ - An entry description shows a node's durable form as text, not as an internal token
20
+
3
21
  ## [0.6.4] - 2026-10-15
4
22
 
5
23
  ### Changed
package/README.md CHANGED
@@ -66,12 +66,13 @@ the box.
66
66
  | `undo.discardCaptured()` | drain and throw away the captured records (failure path companion) |
67
67
  | `undo.flush()` | force-close the current idle batch as its own commit |
68
68
  | `undo.command(label, fn)` | run a synchronous `fn` and push only its own writes as one labelled entry; returns `{ id, label }`, or `null` when it wrote nothing |
69
+ | `undo.trackContent(root, { read, write, prop })` | record `root`'s content as one guarded value step: `read(root)` is sampled when a mutation lands inside the root and `write(root, html)` replays a step, so the inside of a tracked root is never recorded as raw text or node steps. Returns a release function; `stop()` forgets every tracked root. `prop` defaults to `'content'` |
69
70
  | `undo.setRecording(mode)` | `'automatic'` (default) or `'commands'`; see [Recording modes](#recording-modes) |
70
- | `undo.undo()` / `undo.redo()` | navigate history; each returns a receipt, `{ ok: true, entryId, label, direction }` or `{ ok: false, code, entryId, label, direction }`. `undo({ entryId })` and `redo({ entryId })` replay only while that entry is the next one, and refuse `stale-entry` otherwise |
71
+ | `undo.undo()` / `undo.redo()` | navigate history; each returns a receipt, `{ ok: true, entryId, label, direction, skipped, caret }` or `{ ok: false, code, entryId, label, direction }`. `skipped` is the list of skip records from this call and is present only when something was skipped, and a press that only skipped returns `{ ok: false, code: 'empty', direction, skipped }`. `caret` says where the applied entry changed text (see [Where the text changed](#where-the-text-changed)); it is present only when the applied entry changed text. `undo({ entryId })` and `redo({ entryId })` replay only while that entry is the next one, refuse `stale-entry` otherwise, and never skip |
71
72
  | `undo.discard(entryId)` | drop the refused top entry, and only that one: `{ ok: true, entryId, direction }`, or `{ ok: false, code: 'not-refused' }` |
72
73
  | `undo.clear()` | clear both stacks |
73
74
  | `undo.pause()` / `undo.resume()` | recorder skips while paused |
74
- | `undo.on('undo'\|'redo'\|'commit'\|'refused'\|'discard'\|'clear', fn)` / `undo.off(...)` | subscribe to lifecycle events; `undo`/`redo` fire after a navigate with the receipt, `refused` with `{ entryId, label, direction, code, targets }`, `commit` after a new commit, `discard` with `{ entryId, label, direction }`, `clear` after a reset. (No generic `change` event.) |
75
+ | `undo.on('undo'\|'redo'\|'commit'\|'refused'\|'skipped'\|'discard'\|'clear', fn)` / `undo.off(...)` | subscribe to lifecycle events; `undo`/`redo` fire after a navigate with the receipt, `refused` with `{ entryId, label, direction, code, targets }`, `skipped` with `{ entryId, label, direction, code, targets, description }`, `commit` after a new commit, `discard` with `{ entryId, label, direction }`, `clear` after a reset. (No generic `change` event.) |
75
76
  | `undo.canUndo` / `undo.canRedo` | booleans (getters) |
76
77
  | `undo.history` | `[{ id, label, timestamp }, ...]`, oldest first; `timestamp` is `Date.now()` millis. The entry that was just refused also carries `refused: { direction, code, targets }` |
77
78
  | `undo.future` | the redo stack in the same shape, next redo first |
@@ -90,9 +91,15 @@ the box.
90
91
  | `idleLabel` | `'Edit'` | label for auto-closed batches |
91
92
  | `bindKeys` | `true` (singleton), `false` (`create`) | install the global Cmd+Z handler |
92
93
  | `shadowKeydownIn` | code-editor selectors (see below) | when `event.target.closest(selector)` matches, the global handler bails without `preventDefault` |
94
+ | `editableKeys` | `'native'` | how the history keys and the browser's own history input event are routed inside editable content. `'native'` leaves every `contenteditable` (and the `shadowKeydownIn` list) its own undo. `'page'` sends Cmd+Z, Cmd+Shift+Z, Ctrl+Y and a `beforeinput` `historyUndo`/`historyRedo` to the page history when the target sits inside a tracked content root or matches `ownKeysIn`; a form control, the `shadowKeydownIn` list and an editing host the scope does not record keep their own undo. Only then is `beforeinput` listened to at all |
95
+ | `ownKeysIn` | `null` | selectors whose history keys and history input events belong to the page stack under `editableKeys: 'page'`, even when the element also matches `shadowKeydownIn` (a block editor sharing its element with `.ProseMirror`) |
96
+ | `typingBreak` | `null` | `'sentence'` closes the open typing step after the space or line break that follows `.`, `?` or `!` (the delimiter belongs to the finished sentence), at a switch to another field, at a switch between typing and deleting, after a paste, a drop, a cut or a format command, when a selection is replaced, and at a caret jump made with the mouse or a navigation key. So one undo press undoes one sentence of typing. On every scope an open IME composition holds the idle step open |
97
+ | `holdReplay` | `null` | function `() => boolean`; while it answers true, `undo()` and `redo()` return `{ ok: false, code: 'held' }` without flushing, moving either stack, changing the document or emitting `refused` |
98
+ | `refused` | `null` | `'skip'` lets one `undo()` or `redo()` press get past a step it can no longer replay: the refused entry is removed from its stack (it goes to no other stack), announced with a `skipped` event, and the same call goes on to the next entry until one applies or the stack is empty. Every refusal code is skipped, `replay-error` included, but `undo({ entryId })` never skips. The default keeps a refused entry on top. See [Refusals](#refusals) |
93
99
  | `ignoreAttribute` | `null` | predicate `(attrName, element) => boolean`; return true to skip recording that attribute mutation, and the certificate's projection skips it too, so such a change never refuses a later undo or redo. |
94
100
  | `isProtected` | `null` | predicate `(node) => boolean`; return true for content that no undo or redo may erase, and an operation that would remove or replace a subtree containing such a node is refused with code `"protected"`. |
95
101
  | `ignoreNode` | `null` | predicate `(node) => boolean`; in automatic mode a `childList` record whose every added and removed node it accepts is not recorded, so a gesture's own repeated moves leave no generic entry beside the one the consumer records with `command`, and such a node is left out of a bracketed entry's added and removed lists too. |
102
+ | `durableForm` | `null` | function `(node, host) => string \| undefined`; return a string for a node whose live markup is not its saved markup (a mounted rich text editor), and the certificate uses that string as the node's whole form instead of looking inside it, at record time and at every replay check. Return `undefined` to keep the default form. A throw or a non-string also keeps the default. |
96
103
  | `debug` | `false` | console.log internal state transitions |
97
104
 
98
105
  ## Batching
@@ -141,6 +148,39 @@ undo.undo() // { ok: true, entryId, label, direction }
141
148
 
142
149
  A refused entry keeps its place on top, and `undo.history` (or `undo.future`) shows `refused: { direction, code, targets }` on it, so nothing is lost and nothing is silently skipped. `undo.discard(entryId)` drops exactly that entry and leaves the document and every other entry as they are.
143
150
 
151
+ ### Skipping what can no longer be replayed
152
+
153
+ By default a refused entry keeps its place on top, so every later press refuses it again until it is dismissed. With `refused: 'skip'` one press removes it instead and goes on:
154
+
155
+ ```js
156
+ const undo = hyperundo.create({ refused: 'skip' })
157
+ undo.on('skipped', ({ entryId, label, direction, code, targets, description }) => keepCopy(description))
158
+
159
+ undo.undo()
160
+ // { ok: true, entryId: 'A', label: 'Edit A', direction: 'undo',
161
+ // skipped: [{ entryId: 'C', label: 'Edit C', direction: 'undo', code: 'content-changed',
162
+ // targets: [...], description: { id: 'C', label: 'Edit C', ... } }] }
163
+ ```
164
+
165
+ The press pops each refused entry, emits one `skipped` event for it and continues to the next entry in the same call, until one applies or the stack is empty (`{ ok: false, code: 'empty', direction, skipped }`). A skipped entry goes to no other stack and emits no `refused` event. Its `description` is the plain-data `describe(entryId)` of the entry, built before it is removed, so a consumer can keep a recovery copy after the fact. `undo({ entryId })` never skips: a named entry is refused as it is today.
166
+
167
+ ### Where the text changed
168
+
169
+ A successful receipt carries `caret`, taken from the last operation of the replay that changed text, so a host can put the caret where the removed or restored text was. The key is present only when the applied entry changed text:
170
+
171
+ | The entry changed | `caret` |
172
+ |---|---|
173
+ | a text node | `{ node, offset }`, at the end of the changed run of its data |
174
+ | a tracked content root (`trackContent`) | `{ root, textOffset }`, at the end of the changed run of `root.textContent` |
175
+ | added or removed a text node | `{ node, offset }`: the parent at the node's index, or the node at its length |
176
+ | a value step with a `write` and no `content` flag, an attribute, a node that is not text | no `caret` key |
177
+
178
+ ```js
179
+ undo.undo() // { ok: true, entryId, label, direction, skipped, caret: { node, offset: 5 } }
180
+ ```
181
+
182
+ The engine never reads or writes the selection; placing the caret is the host's job. A refused or skipped press reports no `caret`, and when one press skips entries and then applies one, the `caret` is the applied entry's.
183
+
144
184
  `targets` is one `{ id, kind }` per node the refused step addresses. `id` is the nearest `[data-id]` ancestor, so a text step with no id of its own names the record it was typed in, and `null` when nothing in the ancestor chain carries one; `kind` says what the target was to the step (`added`, `removed`, `move`, `protected`, or the attribute or property name of a value step). The receipt's own `targets` are on the `refused` event and the history entry, so a consumer can name the record a refusal is about without asking the engine again.
145
185
 
146
186
  ## Describing an entry
@@ -198,6 +238,32 @@ records nothing: the page's Cmd+Z history starts at the write a commit or a
198
238
  `command` makes of that text. Pass `bindKeys: false` to opt out entirely and
199
239
  bind your own handler.
200
240
 
241
+ ### One undo press, one sentence of typing
242
+
243
+ `typingBreak: 'sentence'` groups typing into sentences instead of into half
244
+ second batches. A step closes after the space or line break that follows `.`,
245
+ `?` or `!`, so the space belongs to the finished sentence; the rule is simple on
246
+ purpose, so `e.g. ` ends a step early and `3.5` does not. It also closes at a
247
+ switch to another field, at a switch between typing and deleting, after a paste,
248
+ a drop, a cut or a format command (each is a step of its own), when a selection
249
+ is replaced, and at a caret jump made with the mouse or with the navigation
250
+ keys. An open IME composition never lets the idle timer close a step, with or
251
+ without the option.
252
+
253
+ `endsSentence(textBefore)` is exported for a host that needs the same rule.
254
+
255
+ ### Editable content can share the page history
256
+
257
+ `editableKeys: 'page'` gives `contenteditable` fields the page's timeline. A key
258
+ or the browser's own Undo (`beforeinput` `historyUndo`) inside a plain field, a
259
+ tracked rich field, or an element named by `ownKeysIn` calls `undo()` or
260
+ `redo()`; the event is prevented, so the browser's per-field undo never runs and
261
+ the two histories cannot drift. An `input`, a `textarea`, a `select`, an open
262
+ IME composition, the `shadowKeydownIn` list, and an editing host inside a
263
+ region the scope does not record (an authored `no-undo` field) keep their own
264
+ undo. An empty page history still prevents the key: the field's own undo would
265
+ change text the page history does not know about.
266
+
201
267
  ## Filter attributes
202
268
 
203
269
  A mutation is excluded from recording when its target's ancestor chain contains
@@ -1 +1 @@
1
- var hyperundo=(()=>{var Y=Object.defineProperty;var mt=Object.getOwnPropertyDescriptor;var pt=Object.getOwnPropertyNames;var gt=Object.prototype.hasOwnProperty;var ht=(e,n)=>{for(var t in n)Y(e,t,{get:n[t],enumerable:!0})},yt=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of pt(n))!gt.call(e,i)&&i!==t&&Y(e,i,{get:()=>n[i],enumerable:!(r=mt(n,i))||r.enumerable});return e};var bt=e=>yt(Y({},"__esModule",{value:!0}),e);var Qt={};ht(Qt,{default:()=>Zt,undo:()=>oe});function wt(e,n=null){let t=[];if(e.type==="attributes"){let r=e.target,i=e.attributeName,a=e.oldValue,s=r.getAttribute(i);return a==null&&s!=null?t.push({kind:"attr-add",target:r,name:i,newValue:s}):a!=null&&s==null?t.push({kind:"attr-remove",target:r,name:i,oldValue:a}):a!==s&&t.push({kind:"attr-set",target:r,name:i,oldValue:a,newValue:s}),t}if(e.type==="characterData")return t.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),t;if(e.type==="childList"){let r=e.target,i=e.nextSibling,a=e.nextSibling;for(;a&&n?.(a);)a=a.nextSibling;let s=e.previousSibling;for(;s&&n?.(s);)s=s.previousSibling;let l=n?Array.from(e.addedNodes).filter(h=>!n(h)):Array.from(e.addedNodes),c=n?Array.from(e.removedNodes).filter(h=>!n(h)):Array.from(e.removedNodes);return l.length>0&&t.push({kind:"add",parent:r,nodes:l,before:i,afterAuthored:a,beforeAuthored:s,record:e}),c.length>0&&t.push({kind:"remove",parent:r,nodes:c,before:i,afterAuthored:a,beforeAuthored:s,record:e}),t}return t}function Ee(e,n=null){let t=[],r=new Map;for(let i of e){if(i.type==="attributes"){let a=i.target,s=i.attributeName,l=r.get(a);l||(l=new Map,r.set(a,l)),l.has(s)||(l.set(s,{idx:t.length,oldValue:i.oldValue}),t.push(null));continue}for(let a of wt(i,n))t.push(a)}for(let[i,a]of r)for(let[s,l]of a){let c=l.oldValue,h=i.getAttribute(s),p=null;c===h?p=null:c==null?p={kind:"attr-add",target:i,name:s,newValue:h}:h==null?p={kind:"attr-remove",target:i,name:s,oldValue:c}:p={kind:"attr-set",target:i,name:s,oldValue:c,newValue:h},t[l.idx]=p}return t.filter(i=>i!=null)}function Se(e){let n=[],t=new Map;for(let i of e){let a=Ae(i);if(a==null){n.push(i);continue}let s=t.get(i.target);s||(s=new Map,t.set(i.target,s));let l=s.get(a);if(l==null){let c=Ne(i);if(c.oldValue===c.newValue)continue;s.set(a,n.length),n.push({...i,...c})}else n[l]={...n[l],newValue:Ne(i).newValue}}let r=[];for(let i of n){if(Ae(i)==null){r.push(i);continue}i.oldValue!==i.newValue&&r.push(vt(i))}return r}var kt=new Set(["attr-set","attr-add","attr-remove","text","value"]);function Ae(e){return kt.has(e.kind)?e.kind==="text"?"#text":e.kind==="value"?`#${e.prop||"value"}`:e.name==null?null:`@${e.name}`:null}function Ne(e){return e.kind==="attr-add"?{oldValue:null,newValue:e.newValue}:e.kind==="attr-remove"?{oldValue:e.oldValue,newValue:null}:{oldValue:e.oldValue,newValue:e.newValue}}function vt(e){return e.kind!=="attr-add"&&e.kind!=="attr-remove"?e:e.oldValue==null&&e.newValue!=null?{...e,kind:"attr-add"}:e.oldValue!=null&&e.newValue==null?{...e,kind:"attr-remove"}:{...e,kind:"attr-set"}}var Te=new WeakMap,At=0;function T(e){let n=Te.get(e);return n||(n=`n${++At}`,Te.set(e,n)),n}function v(e){return!!e&&typeof e=="object"&&typeof e.nodeType=="number"}function Nt(e){return e.kind==="add"||e.kind==="remove"}function Ie(e,n){let t=[],r=e.childNodes;if(!r)return t;for(let i of r)n&&n(i)||t.push(T(i));return t}function K(e,n,t=null,r=null){if(!v(e))return null;if(e.nodeType===3)return`#text:${JSON.stringify(e.data)}`;if(e.nodeType===8)return`#comment:${JSON.stringify(e.data)}`;if(e.nodeType!==1)return`#node:${e.nodeType}`;let i=[];if(e.attributes){for(let l of e.attributes)Et(l.name,e,n,t)||i.push(`${l.name}=${JSON.stringify(l.value)}`);i.sort()}let a=[];if(e.childNodes)for(let l of e.childNodes)St(l,r)||a.push(K(l,n,t,r));return`<${String(e.tagName||"").toLowerCase()}${i.length?` ${i.join(" ")}`:""}>${a.join("")}</>`}function Z(e,n=[]){if(!v(e))return n;if(e.nodeType===1){let t=e.getAttribute&&e.getAttribute("data-id");t&&n.push(t)}if(e.childNodes)for(let t of e.childNodes)Z(t,n);return n}function Et(e,n,t,r=null){if(typeof t!="function")return!1;try{return!!t(e,n,r)}catch{return!1}}function St(e,n){if(typeof n!="function")return!1;try{return!!n(e)}catch{return!1}}function Pe(e,n){if(typeof n!="function")return[];let t=[],r=i=>{if(!v(i))return;let a=!1;try{a=!!n(i)}catch{a=!1}if(a&&t.push(E(i,"protected")),i.childNodes)for(let s of i.childNodes)r(s)};for(let i of e)r(i);return t}function E(e,n,t=null){let r=e;r&&r.nodeType!==1&&(r=r.parentElement);let i=r&&typeof r.closest=="function"?r.closest("[data-id]"):null;return!i&&t&&typeof t.closest=="function"&&(i=t.closest("[data-id]")),{id:(i?i.getAttribute("data-id"):null)||null,kind:n||null}}function Tt(e,n,t=null){return(e||[]).filter(v).map(r=>E(r,n,t))}function z(e){return{replayable:!1,reason:e,steps:[]}}function U(e,n){let t=[[n.afterAuthored,i=>i],[n.beforeAuthored,i=>i+1],[n.before,i=>i]],r=!1;for(let[i,a]of t){if(!v(i))continue;r=!0;let s=e.indexOf(T(i));if(s>=0)return a(s)}return r?null:e.length}function Ce(e,n={}){let t=n.isIgnored||null,r=n.ignoreAttribute||null,i=Se(e),a=[];for(let p of i){if(!Nt(p)){if(!v(p.target))return z("non-dom-target");continue}if(!v(p.parent))return z("non-dom-parent");let y=Array.from(p.nodes||[]).filter(v);if(y.length!==0){if(y.length!==Array.from(p.nodes||[]).length)return z("non-dom-node");a.push({primitive:p,nodes:y,parent:p.parent})}}if(Vt(a))return z("in-record-move");let s=It(a,t);if(!s)return z("ambiguous-structure");let l=Pt(a,s),c=new Map(a.map(p=>[p.primitive,p])),h=[];for(let p of i){let y=c.get(p);if(!y){h.push(Rt(p));continue}let w=l.get(y);if(w){w.at===y&&h.push(w.step);continue}h.push(Ct(y,s.get(y),r,t))}return{replayable:!0,reason:null,steps:h}}function Vt(e){let n=new Map;for(let t of e){let r=t.primitive.record;if(!r)continue;let i=n.get(r);i||(i=new Map,n.set(r,i));for(let a of t.nodes){let s=i.get(a);s||(s=new Set,i.set(a,s)),s.add(t.primitive.kind)}}for(let t of n.values())for(let r of t.values())if(r.size>1)return!0;return!1}function It(e,n){let t=new Map;for(let i of e)t.has(i.parent)||t.set(i.parent,Ie(i.parent,n));let r=new Map;for(let i=e.length-1;i>=0;i--){let a=e[i],s=t.get(a.parent);if(!s)return null;let l=s.slice(),c;if(a.primitive.kind==="add"){let h=a.nodes.map(T);if(!h.every(p=>s.includes(p)))return null;c=s.filter(p=>!h.includes(p))}else{let h=U(l,a.primitive);if(h==null)return null;c=l.slice(),c.splice(h,0,...a.nodes.map(T))}r.set(a,{before:c,after:l}),t.set(a.parent,c)}return r}function Pt(e,n){let t=new Map,r=new Map;e.forEach((a,s)=>{for(let l of a.nodes){let c=T(l);a.primitive.kind==="remove"?t.set(c,{item:a,index:s,node:l}):r.set(c,{item:a,index:s,node:l})}});let i=new Map;for(let[a,s]of t){let l=r.get(a);if(!l||l.index<s.index||s.item.nodes.length!==1||l.item.nodes.length!==1)continue;let c={type:"move",node:s.node,sourceParent:s.item.parent,destParent:l.item.parent,source:n.get(s.item),dest:n.get(l.item),sourceSlot:s.item.primitive,destSlot:l.item.primitive,targets:[E(s.node,"move")]};i.set(s.item,{step:c,at:l.item}),i.set(l.item,{step:c,at:l.item})}return i}function Ct(e,n,t,r){let i=e.primitive.kind;return{type:i==="add"?"add":"restore",parent:e.parent,nodes:e.nodes,seqBefore:n.before,seqAfter:n.after,slot:e.primitive,content:e.nodes.map(a=>K(a,t,e.parent,r)),ids:i==="remove"?e.nodes.flatMap(a=>Z(a)):[],targets:Tt(e.nodes,i==="add"?"added":"removed",e.parent)}}function Rt(e){return{type:e.kind,primitive:e,targets:[E(e.target,e.name||e.prop||"text")]}}function Re(e,n,t={}){if(!e||!e.replayable)return{ok:!1,code:"unsupported",targets:[]};let r={isIgnored:t.isIgnored||null,ignoreAttribute:t.ignoreAttribute||null,isProtected:t.isProtected||null,inScope:t.inScope||(()=>!0),liveNodesWithIds:t.liveNodesWithIds||(()=>[]),sim:new Map,planned:new Map,operations:[]};for(let i=0;i<e.steps.length;i++){let a=e.steps[n==="undo"?e.steps.length-1-i:i],s=xt(a,n,r);if(s)return{ok:!1,code:s.code,targets:s.targets}}return{ok:!0,operations:r.operations}}function M(e,n){return e.sim.has(n)||e.sim.set(n,Ie(n,e.isIgnored)),e.sim.get(n)}function L(e,n){for(let t=e;t;t=t.parentNode)if(n.planned.has(t))return n.planned.get(t);return n.inScope(e)}function Oe(e,n){return n.liveNodesWithIds(e).filter(t=>L(t,n)).length>0}function b(e,n){return{code:e,targets:n||[]}}function Ot(e){return e.kind==="text"?e.target.data:e.kind==="value"?typeof e.read=="function"?e.read(e.target):e.target[e.prop||"value"]:e.target.getAttribute(e.name)}function xt(e,n,t){let r=n==="undo";return e.type==="move"?zt(e,r,t):e.type==="add"?Mt(e,r,t):e.type==="restore"?Lt(e,r,t):_t(e,r,t)}function _t(e,n,t){let r=e.primitive,i=n?r.newValue:r.oldValue,a=n?r.oldValue:r.newValue;return v(r.target)?L(r.target,t)?r.kind==="value"&&typeof r.write=="function"&&typeof r.read!="function"?b("unsupported",e.targets):Ot(r)!==i?b("content-changed",e.targets):(r.kind==="value"?t.operations.push({type:"value",target:r.target,prop:r.prop||"value",value:a,read:r.read||null,write:r.write||null}):r.kind==="text"?t.operations.push({type:"text",target:r.target,data:a}):t.operations.push({type:"attr",target:r.target,name:r.name,value:a??null}),null):b("target-detached",e.targets):b("unsupported",e.targets)}function Mt(e,n,t){if(!v(e.parent)||!L(e.parent,t))return b("target-detached",e.targets);if(n){if(!q(M(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);let l=Pe(e.nodes,t.isProtected);if(l.length>0)return b("protected",l);for(let c=0;c<e.nodes.length;c++)if(K(e.nodes[c],t.ignoreAttribute,e.parent,t.isIgnored)!==e.content[c])return b("content-changed",e.targets);for(let c of e.nodes)t.operations.push({type:"remove",node:c}),t.planned.set(c,!1);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!q(M(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);for(let l of e.nodes)if(l.parentNode)return b("structure-changed",e.targets);let r=e.nodes.flatMap(l=>Z(l));if(r.length>0&&Oe(r,t))return b("duplicate-id",e.targets);let i=U(e.seqBefore,e.slot);if(i==null)return b("unsupported",e.targets);let a=e.seqBefore.slice(),s=i;for(let l of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:l,beforeId:a[s]??null}),a=a.slice(0,s).concat(T(l),a.slice(s)),s+=1,t.planned.set(l,!0);return t.sim.set(e.parent,e.seqAfter.slice()),null}function Lt(e,n,t){if(!v(e.parent)||!L(e.parent,t))return b("target-detached",e.targets);if(n){if(!q(M(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);for(let c=0;c<e.nodes.length;c++){let h=e.nodes[c];if(h.parentNode||K(h,t.ignoreAttribute,e.parent,t.isIgnored)!==e.content[c])return b("content-changed",e.targets)}if(e.ids.length>0&&Oe(e.ids,t))return b("duplicate-id",e.targets);let a=U(e.seqAfter,e.slot);if(a==null)return b("unsupported",e.targets);let s=e.seqAfter.slice(),l=a;for(let c of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:c,beforeId:s[l]??null}),s=s.slice(0,l).concat(T(c),s.slice(l)),l+=1,t.planned.set(c,!0);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!q(M(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);let r=Pe(e.nodes,t.isProtected);if(r.length>0)return b("protected",r);let i=a=>t.planned.get(a)===!0||typeof t.isIgnored=="function"&&t.isIgnored(a);for(let a=0;a<e.nodes.length;a++){let s=e.nodes[a];if(s.parentNode!==e.parent||K(s,t.ignoreAttribute,e.parent,i)!==e.content[a])return b("content-changed",e.targets)}for(let a of e.nodes)t.operations.push({type:"remove",node:a}),t.planned.set(a,!1);return t.sim.set(e.parent,e.seqAfter.slice()),null}function qt(e,n){let t=e.indexOf(n);return t<0?null:e.slice(0,t).concat(e.slice(t+1))}function zt(e,n,t){let r=T(e.node),i=n?e.destParent:e.sourceParent,a=n?e.sourceParent:e.destParent,s=M(t,i),l=M(t,a),c=n?e.dest.after:e.source.before,h=n?e.source.after:e.dest.before,p=n?e.sourceSlot:e.destSlot;if(!v(i)||!v(a)||!L(i,t)||!L(a,t))return b("target-detached",e.targets);if(!q(s,c))return b("structure-changed",e.targets);let y=i===a?qt(l,r):l;if(!q(y,h))return b("structure-changed",e.targets);let w=U(h,p);return w==null?b("unsupported",e.targets):(t.operations.push({type:"insert",parent:a,node:e.node,beforeId:h[w]??null}),t.planned.set(e.node,!0),n?(t.sim.set(a,e.source.before.slice()),i!==a&&t.sim.set(i,e.dest.before.slice())):(t.sim.set(a,e.dest.after.slice()),i!==a&&t.sim.set(i,e.source.after.slice())),null)}function q(e,n){if(!e||!n||e.length!==n.length)return!1;for(let t=0;t<e.length;t++)if(e[t]!==n[t])return!1;return!0}function Kt(e,n){for(let t of e.childNodes||[])if(T(t)===n)return t;return null}function Ve(e,n){typeof e.write=="function"?e.write(e.target,n):e.target[e.prop]=n}function xe(e){let n=[];try{for(let t of e)$t(t,n);return{ok:!0}}catch(t){for(let r=n.length-1;r>=0;r--)try{n[r]()}catch{}return{ok:!1,error:t}}}function $t(e,n){if(e.type==="attr"){let t=e.target.getAttribute(e.name);e.value==null?e.target.removeAttribute(e.name):e.target.setAttribute(e.name,e.value),n.push(()=>{t==null?e.target.removeAttribute(e.name):e.target.setAttribute(e.name,t)});return}if(e.type==="text"){let t=e.target.data;e.target.data=e.data,n.push(()=>{e.target.data=t});return}if(e.type==="value"){let t=typeof e.read=="function"?e.read(e.target):e.target[e.prop];Ve(e,e.value),n.push(()=>Ve(e,t));return}if(e.type==="insert"){let t=e.beforeId==null?null:Kt(e.parent,e.beforeId);if(e.beforeId!=null&&!t)throw new Error("hyper-undo: replay anchor vanished");let r=e.node.parentNode,i=e.node.nextSibling;e.parent.insertBefore(e.node,t),n.push(()=>{r?r.insertBefore(e.node,i&&i.parentNode===r?i:null):e.node.parentNode===e.parent&&e.parent.removeChild(e.node)});return}if(e.type==="remove"){let t=e.node.parentNode;if(!t)throw new Error("hyper-undo: replay target is no longer attached");let r=e.node.nextSibling;t.removeChild(e.node),n.push(()=>t.insertBefore(e.node,r))}}var Bt=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],Dt=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],_e=Bt.join(","),Me=new RegExp("^(?:"+Dt.join("|")+")(?:-|$)");var an=Object.freeze({data:Object.freeze({tokens:["no-data"],bundles:["editor-ui"]}),save:Object.freeze({tokens:["no-save"],bundles:["editor-ui"]}),snapshot:Object.freeze({tokens:["no-snapshot"],bundles:["editor-ui"]}),watch:Object.freeze({tokens:["no-watch"],bundles:["editor-ui"]}),undo:Object.freeze({tokens:["no-undo"],bundles:["editor-ui"]}),history:Object.freeze({tokens:[],bundles:["editor-ui"]})}),Ut=Object.freeze({"editor-ui":Object.freeze(["no-data","no-save","no-snapshot","no-watch","no-undo"])}),jt=Object.freeze(["no-save","no-snapshot","no-trigger-autosave","no-dirty","no-watch","no-undo","no-data","freeze","editor-ui","live"]);function j(e,n){if(!e||e.nodeType!==1)return!1;let t=e.getAttribute?.("clay");return!!(t&&t.split(/\s+/).includes(n)||e.hasAttribute?.(n))}function Q(e,n){if(j(e,n))return!0;for(let[t,r]of Object.entries(Ut))if(r.includes(n)&&j(e,t))return!0;return!1}function Le(e){return e==="clay"||jt.includes(e)}var Ft=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function Wt(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function ee(e){let n=Wt();if(n)try{if(!n(e).undoable)return!0}catch{}let t=e&&e.nodeType!==1?e.parentElement:e;if(t&&t.closest&&t.closest(_e))return!0;for(;t&&t.nodeType===1;){if(Q(t,"no-undo")||Q(t,"no-watch"))return!0;for(let r of Ft)if(j(t,r))return!0;t=t.parentElement}return!1}function Xt(e){let n=e.target,t=n.cloneNode(!1),r=t;for(;n.parentElement;){n=n.parentElement;let i=n.cloneNode(!1);i.appendChild(t),t=i}return e.oldValue==null?r.removeAttribute(e.attributeName):r.setAttribute(e.attributeName,e.oldValue),ee(r)}function F(e,n,t){if(t?.type==="attributes"&&Le(t.attributeName)){if(ee(e)&&Xt(t))return!0}else if(ee(e))return!0;if(t&&t.type==="attributes"){if(t.attributeName&&Me.test(t.attributeName.toLowerCase()))return!0;if(n)try{if(n(t.attributeName,t.target))return!0}catch{}}return!1}function qe(){let e=new Map;function n(i,a){let s=e.get(i);return s||(s=new Set,e.set(i,s)),s.add(a),()=>t(i,a)}function t(i,a){let s=e.get(i);s&&s.delete(a)}function r(i,a){let s=e.get(i);if(s)for(let l of Array.from(s))try{l(a)}catch(c){console.error("[hyper-undo] listener threw",c)}}return{on:n,off:t,emit:r}}var Ht={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",recording:"automatic",ignoreAttribute:null,isProtected:null,ignoreNode:null,debug:!1};function te(e){let n={...Ht,...e};if(!n.scope)throw new Error("hyper-undo: scope is required");let t=qe(),r=[],i=[],a=`u${Math.random().toString(36).slice(2,8)}`,s=0,l=null,c=[],h=null,p=null,y=0,w=!1,P=n.recording==="commands"?"commands":"automatic",A=null,C=!1;function k(...o){n.debug&&console.log("[hyper-undo]",...o)}function De(){w||(w=!0,p=Ue(R),p.observe(n.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),k("started"))}function Ue(o){let u=typeof document<"u"&&n.scope===document.body,d=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return u&&d&&typeof d.createObserver=="function"?(k("sourcing records from the platform Mutation hub (shared observer)"),d.createObserver(o)):new MutationObserver(o)}function ie(){w&&(w=!1,p&&(p.disconnect(),p=null),O(),r.length=0,i.length=0,c=[],l=null,A=null,C=!1,y=0,k("stopped"))}function je(){let o=n.scope;return!!(o&&o.nodeType===1&&o.isConnected===!1)}function R(o){ae(o,!1)}function ae(o,u){if(A){for(let f of o)A.records.push(f);return}if(P==="commands"||y>0)return;let d=W(Fe(o),u);if(d.length>0){for(let f of d)c.push(f);ue()}}function Fe(o){return typeof n.ignoreNode!="function"?o:o.filter(u=>{if(u.type!=="childList")return!0;let d=[...u.addedNodes,...u.removedNodes];return d.length===0?!0:!d.every(f=>We(f))})}function We(o){try{return!!n.ignoreNode(o)}catch{return!1}}function W(o,u=!1){let d=lt(o),f=o.filter(g=>(G(g.target)||d(g.target)||u&&ke(g.target))&&!F(g.target,n.ignoreAttribute,g));return Ee(f,g=>F(g,n.ignoreAttribute))}function ue(){O(),h=setTimeout(se,n.idleWindowMs)}function O(){h!=null&&(clearTimeout(h),h=null)}function se(){if(h=null,c.length===0)return;let o=c;c=[],$(n.idleLabel,o)}function $(o,u){let d=Ce(u,{isIgnored:X,ignoreAttribute:le});if(d.steps.length===0)return k("commit",o,"with nothing to record"),t.emit("commit"),null;let f={id:`${a}-${++s}`,label:o,timestamp:Date.now(),primitives:u,certificate:d};for(r.push(f),i.length=0,l=null;r.length>n.maxHistory;)r.shift();return k("commit",o,"primitives:",u.length),t.emit("commit"),f}function X(o){return F(o,n.ignoreAttribute)}function le(o,u,d=null){if(typeof n.ignoreAttribute!="function")return!1;try{return!!n.ignoreAttribute(o,u,d)}catch{return!1}}function Xe(o){if(typeof n.isProtected!="function")return!1;try{return!!n.isProtected(o)}catch{return!1}}function He(o,u){if(!w)throw new Error("hyper-undo: scope not started");if(P==="commands")return fe(o,u,{honorPause:!0,receipt:!1});R(p.takeRecords()),x();let d=y===0?de():null,f=u();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(ae(p.takeRecords(),!0),d!=null&&ce(d),c.length===0){t.emit("commit");return}O();let g=c;c=[],$(o,g)}function Je(o){if(!w)throw new Error("hyper-undo: scope not started");if(!p)return;if(y>1){p.takeRecords();return}x();let u=W(p.takeRecords(),!0);u.length!==0&&(O(),$(o,u))}function Ge(){p&&p.takeRecords()}function H(){let o=n.scope;return o&&o.ownerDocument||(typeof document<"u"?document:null)}function B(){let o=H();return!o||!o.head||typeof o.head.querySelector!="function"?null:o.head.querySelector("title")}function de(){let o=B();return o?o.textContent:null}function ce(o){let u=B();if(!u||o==null)return;let d=u.textContent;d!==o&&pe({kind:"value",target:u,prop:"textContent",oldValue:o,newValue:d})}function Ye(o){if(!o)return null;let u=H();if(!u||o.target!==u||o.prop!=="title")return o;let d=B();return d?{kind:"value",target:d,prop:"textContent",oldValue:o.oldValue,newValue:o.newValue}:null}function Ze(){if(typeof window>"u")return!1;let o=window.clay?.effect||window.hyperclay?.effect;return!!(o&&o.running)}function Qe(){p&&R(p.takeRecords()),x(),A={records:[],primitives:[]}}function et(){let o=A;if(A=null,!o)return[];let u=W(o.records,!0);for(let d of o.primitives)u.push(d);return u}function fe(o,u,{honorPause:d,receipt:f}){if(typeof u!="function")throw new Error("hyper-undo: command() needs a synchronous function");if(A)throw new Error("hyper-undo: command() cannot nest inside another command");if(C)throw new Error("hyper-undo: command() cannot run during undo or redo");if(Ze())throw new Error("hyper-undo: command() cannot run inside a derived-effect context");if(u.constructor&&u.constructor.name==="AsyncFunction")throw new Error("hyper-undo: command() fn must be synchronous; it was declared async");let g=d&&y>0,N=g?null:de();Qe();let S=[],V=null,_;try{try{_=u()}catch(ve){V=ve}if(p&&R(p.takeRecords()),_&&typeof _.then=="function"){try{_.then(void 0,()=>{})}catch{}V||(V=new Error("hyper-undo: command() fn must be synchronous; returned a Promise"))}g||ce(N)}finally{S=et()}let D=g||S.length===0?null:$(o,S);if(V)throw V;return f?D?{id:D.id,label:D.label}:null:D}function tt(o,u){if(!w)throw new Error("hyper-undo: scope not started");return fe(o,u,{honorPause:!1,receipt:!0})}function nt(o){if(o!=="automatic"&&o!=="commands")throw new Error("hyper-undo: setRecording() takes 'automatic' or 'commands'");if(A)throw new Error("hyper-undo: setRecording() cannot change while a command is running");return P===o||(x(),P=o,k("recording",o)),P}function me(o){if(!w||!o||!A&&(y>0||P==="commands"))return;let u=Ye(o);u&&pe(u)}function pe(o){if(A){A.primitives.push(o);return}c.push(o),ue()}function rt(o,{prop:u="value",oldValue:d,newValue:f,read:g,write:N}={}){!o||d===f||me({kind:"value",target:o,prop:u,oldValue:d,newValue:f,read:g,write:N})}function x(){y===0&&p&&R(p.takeRecords()),c.length!==0&&(O(),se())}function ge(){y===0&&p&&R(p.takeRecords()),y++,k("paused",y)}function he(){y!==0&&(y--,y===0&&p&&p.takeRecords(),k("resumed",y))}function ye(o,u){if(je())return ie(),{ok:!1,code:"disconnected"};x();let d=o==="undo"?r:i,f=o==="undo"?i:r,g=d.at(-1);if(!g)return{ok:!1,code:"empty"};if(u&&u!==g.id){let _=be(u)||g;return J(_,o,"stale-entry")}let N;C=!0;try{N=Re(g.certificate,o,{isIgnored:X,ignoreAttribute:le,isProtected:Xe,inScope:dt,liveNodesWithIds:ct})}finally{C=!1}if(!N.ok)return J(g,o,N.code,N.targets);ge();let S;C=!0;try{S=xe(N.operations)}finally{C=!1,he()}if(!S.ok)return k(`${o} failed and was rolled back`,S.error),J(g,o,"replay-error");d.pop(),f.push(g),l=null;let V={ok:!0,entryId:g.id,label:g.label,direction:o};return k(o,g.label),t.emit(o,V),V}function ot(o){return ye("undo",o&&o.entryId)}function it(o){return ye("redo",o&&o.entryId)}function be(o){return r.find(u=>u.id===o)||i.find(u=>u.id===o)||null}function J(o,u,d,f=[]){return l={entryId:o.id,direction:u,code:d,targets:f},k("refused",u,d),t.emit("refused",{entryId:o.id,label:o.label,direction:u,code:d,targets:f}),{ok:!1,code:d,entryId:o.id,label:o.label,direction:u}}function at(o){if(!l||l.entryId!==o)return{ok:!1,code:"not-refused",entryId:o};let u=l.direction,d=u==="undo"?r:i,f=d.at(-1);return!f||f.id!==o?{ok:!1,code:"stale-entry",entryId:o}:(d.pop(),l=null,k("discarded",f.label),t.emit("discard",{entryId:o,label:f.label,direction:u}),{ok:!0,entryId:o,direction:u})}function ut(o){let u=be(o);if(!u||!u.certificate||!u.certificate.replayable)return null;let d=l&&l.entryId===u.id?{direction:l.direction,code:l.code,targets:(l.targets||[]).map(f=>({id:f.id??null,kind:f.kind??null}))}:null;return{id:u.id,label:u.label,timestamp:u.timestamp,refused:d,steps:u.certificate.steps.map(st)}}function st(o){if(o.type==="move")return{kind:"move",node:{id:E(o.node).id},from:{id:E(o.sourceParent).id},to:{id:E(o.destParent).id}};if(o.type==="add"||o.type==="restore")return{kind:o.type==="add"?"add":"remove",parent:{id:E(o.parent).id},nodes:o.nodes.map((N,S)=>({id:E(N).id,html:o.content[S]??null}))};let u=o.primitive,d={id:E(u.target).id},f=u.oldValue??null,g=u.newValue??null;return o.type==="value"?{kind:"value",target:d,prop:u.prop??"value",before:f,after:g}:o.type==="text"?{kind:"text",target:d,before:f,after:g}:o.type==="attr-set"||o.type==="attr-add"||o.type==="attr-remove"?{kind:"attribute",target:d,name:u.name??null,before:f,after:g}:{kind:o.type,target:d,before:f,after:g}}function lt(o){let u=new Set,d=!0;for(;d;){d=!1;for(let f of o)if(!(f.type!=="childList"||!f.removedNodes?.length)&&!(!G(f.target)&&!we(f.target,u)))for(let g of f.removedNodes)u.has(g)||(u.add(g),d=!0)}return f=>!G(f)&&we(f,u)}function we(o,u){for(let d=o;d;d=d.parentNode)if(u.has(d))return!0;return!1}function G(o){let u=n.scope;return!u||!o?!1:o===u||typeof u.contains=="function"&&u.contains(o)}function ke(o){let u=H(),d=u&&u.head;if(!d||!o)return!1;let f=o.nodeType===1?o:o.parentElement;for(;f&&f!==d;f=f.parentElement)if(f.localName==="title")return!1;return f===d}function dt(o){let u=n.scope;return!o||!u?!1:o===u||B()===o||ke(o)?!0:typeof u.contains=="function"?u.contains(o):!1}function ct(o){let u=n.scope;if(!o||o.length===0||typeof u?.querySelectorAll!="function")return[];let d=new Set(o),f=[];for(let g of u.querySelectorAll("[data-id]"))d.has(g.getAttribute("data-id"))&&(X(g)||f.push(g));return f}function ft(){p&&p.takeRecords(),r.length=0,i.length=0,c=[],l=null,O(),k("cleared"),t.emit("clear")}return{_config:n,start:De,stop:ie,commit:He,commitCaptured:Je,discardCaptured:Ge,flush:x,command:tt,setRecording:nt,record:me,recordValue:rt,undo:ot,redo:it,clear:ft,discard:at,describe:ut,pause:ge,resume:he,on:t.on,off:t.off,get canUndo(){return r.length>0},get canRedo(){return i.length>0},get isPaused(){return y>0},get history(){return r.map(o=>{let u={id:o.id,label:o.label,timestamp:o.timestamp};return l&&l.entryId===o.id&&(u.refused={direction:l.direction,code:l.code,targets:l.targets}),u})},get future(){return i.map(o=>{let u={id:o.id,label:o.label,timestamp:o.timestamp};return l&&l.entryId===o.id&&(u.refused={direction:l.direction,code:l.code,targets:l.targets}),u}).reverse()},get scope(){return n.scope}}}var Jt=new Set(["checkbox","radio","button","submit","reset","image","file","range","color"]);function Gt(e){let n=typeof e.composedPath=="function"?e.composedPath():[],t=n.length>0?n:Yt(e.target);for(let r of t){if(!r||r.nodeType!==1)continue;let i=String(r.tagName||"").toUpperCase();if(i==="TEXTAREA"||i==="SELECT"||i==="INPUT"&&!Jt.has(String(r.type||r.getAttribute?.("type")||"text").toLowerCase())||r.isContentEditable===!0)return!0;let a=r.getAttribute&&r.getAttribute("contenteditable");if(a!=null&&String(a).toLowerCase()!=="false")return!0}return!1}function Yt(e){let n=[];for(let t=e;t;t=t.parentNode||(t.host??null))n.push(t);return n}function ze(e,n){let t=r=>{let i=e.scope;if(i&&i.nodeType===1&&i.isConnected===!1){n&&n();return}if(r.defaultPrevented||r.isComposing||r.keyCode===229||Gt(r))return;let a=r.target,s=e._config.shadowKeydownIn;if(a&&a.closest&&Array.isArray(s))for(let h of s)try{if(a.closest(h))return}catch{}if(!(r.metaKey||r.ctrlKey))return;let c=(r.key||"").toLowerCase();if(c==="z"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.undo();return}if(c==="z"&&r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}if(c==="y"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",t,!0),()=>window.removeEventListener("keydown",t,!0)}var ne=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror",".overtype-input","[data-richclay-active]"],m=null,re=null,Ke=null,I=null;function $e(e,n){if(I){if(I.inst===e)return I.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let t=ze(e,n);return I={inst:e,cleanup:t},t}function Be(e){I&&I.inst===e&&(I.cleanup(),I=null)}var oe={defaults:{shadowKeydownIn:ne},start(e={}){let n=e.scope||(typeof document<"u"?document.body:null);if(!n)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(m){if(re===n)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),m;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let t={...e,scope:n,shadowKeydownIn:e.shadowKeydownIn||ne};return m=te(t),m.start(),re=n,e.bindKeys!==!1&&(Ke=$e(m,()=>oe.stop())),m},stop(){m&&(Be(m),Ke=null,m.stop(),m=null,re=null)},create(e={}){let n=e.scope||(typeof document<"u"?document.body:null);if(!n)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let t={...e,scope:n,shadowKeydownIn:e.shadowKeydownIn||ne},r=te(t),i=null,a={_config:r._config,commit:r.commit,command:r.command,setRecording:r.setRecording,commitCaptured:r.commitCaptured,discardCaptured:r.discardCaptured,flush:r.flush,record:r.record,recordValue:r.recordValue,undo:r.undo,redo:r.redo,clear:r.clear,discard:r.discard,describe:r.describe,pause:r.pause,resume:r.resume,on:r.on,off:r.off,get canUndo(){return r.canUndo},get canRedo(){return r.canRedo},get isPaused(){return r.isPaused},get history(){return r.history},get future(){return r.future},get scope(){return r.scope},start(){e.bindKeys&&(i=$e(r,()=>a.stop())),r.start()},stop(){Be(r),i=null,r.stop()}};return a},commit(e,n){return m?m.commit(e,n):void 0},command(e,n){return m?m.command(e,n):void 0},setRecording(e){return m?m.setRecording(e):void 0},commitCaptured(e){return m?m.commitCaptured(e):void 0},discardCaptured(){return m?m.discardCaptured():void 0},record(e){return m?m.record(e):void 0},recordValue(e,n){return m?m.recordValue(e,n):void 0},undo(e){return m?m.undo(e):void 0},redo(e){return m?m.redo(e):void 0},clear(){return m?m.clear():void 0},discard(e){return m?m.discard(e):void 0},describe(e){return m?m.describe(e):void 0},pause(){return m?m.pause():void 0},resume(){return m?m.resume():void 0},flush(){return m?m.flush():void 0},on(e,n){return m?m.on(e,n):void 0},off(e,n){return m?m.off(e,n):void 0},get canUndo(){return m?m.canUndo:!1},get canRedo(){return m?m.canRedo:!1},get history(){return m?m.history:[]},get future(){return m?m.future:[]},get isPaused(){return m?m.isPaused:!1}};var Zt=oe;return bt(Qt);})();
1
+ var hyperundo=(()=>{var he=Object.defineProperty;var Kt=Object.getOwnPropertyDescriptor;var Bt=Object.getOwnPropertyNames;var Ft=Object.prototype.hasOwnProperty;var qt=(e,n)=>{for(var t in n)he(e,t,{get:n[t],enumerable:!0})},zt=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of Bt(n))!Ft.call(e,a)&&a!==t&&he(e,a,{get:()=>n[a],enumerable:!(o=Kt(n,a))||o.enumerable});return e};var $t=e=>zt(he({},"__esModule",{value:!0}),e);var Rn={};qt(Rn,{default:()=>Pn,endsSentence:()=>ke,undo:()=>Ne});function Ut(e,n=null){let t=[];if(e.type==="attributes"){let o=e.target,a=e.attributeName,i=e.oldValue,s=o.getAttribute(a);return i==null&&s!=null?t.push({kind:"attr-add",target:o,name:a,newValue:s}):i!=null&&s==null?t.push({kind:"attr-remove",target:o,name:a,oldValue:i}):i!==s&&t.push({kind:"attr-set",target:o,name:a,oldValue:i,newValue:s}),t}if(e.type==="characterData")return t.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),t;if(e.type==="childList"){let o=e.target,a=e.nextSibling,i=e.nextSibling;for(;i&&n?.(i);)i=i.nextSibling;let s=e.previousSibling;for(;s&&n?.(s);)s=s.previousSibling;let d=n?Array.from(e.addedNodes).filter(y=>!n(y)):Array.from(e.addedNodes),f=n?Array.from(e.removedNodes).filter(y=>!n(y)):Array.from(e.removedNodes);return d.length>0&&t.push({kind:"add",parent:o,nodes:d,before:a,afterAuthored:i,beforeAuthored:s,record:e}),f.length>0&&t.push({kind:"remove",parent:o,nodes:f,before:a,afterAuthored:i,beforeAuthored:s,record:e}),t}return t}function We(e,n=null){let t=[],o=new Map;for(let a of e){if(a.type==="attributes"){let i=a.target,s=a.attributeName,d=o.get(i);d||(d=new Map,o.set(i,d)),d.has(s)||(d.set(s,{idx:t.length,oldValue:a.oldValue}),t.push(null));continue}for(let i of Ut(a,n))t.push(i)}for(let[a,i]of o)for(let[s,d]of i){let f=d.oldValue,y=a.getAttribute(s),p=null;f===y?p=null:f==null?p={kind:"attr-add",target:a,name:s,newValue:y}:y==null?p={kind:"attr-remove",target:a,name:s,oldValue:f}:p={kind:"attr-set",target:a,name:s,oldValue:f,newValue:y},t[d.idx]=p}return t.filter(a=>a!=null)}function je(e,{sameContent:n=(t,o)=>t===o}={}){let t=[],o=new Map;for(let i of e){let s=$e(i);if(s==null){t.push(i);continue}let d=o.get(i.target);d||(d=new Map,o.set(i.target,d));let f=d.get(s);if(f==null){let y=Ue(i);if(y.oldValue===y.newValue)continue;d.set(s,t.length),t.push({...i,...y})}else t[f]={...t[f],newValue:Ue(i).newValue}}let a=[];for(let i of t){if($e(i)==null){a.push(i);continue}i.oldValue!==i.newValue&&(i.kind==="value"&&i.content===!0&&n(i.oldValue,i.newValue)||a.push(jt(i)))}return a}var Wt=new Set(["attr-set","attr-add","attr-remove","text","value"]);function $e(e){return Wt.has(e.kind)?e.kind==="text"?"#text":e.kind==="value"?`#${e.prop||"value"}`:e.name==null?null:`@${e.name}`:null}function Ue(e){return e.kind==="attr-add"?{oldValue:null,newValue:e.newValue}:e.kind==="attr-remove"?{oldValue:e.oldValue,newValue:null}:{oldValue:e.oldValue,newValue:e.newValue}}function jt(e){return e.kind!=="attr-add"&&e.kind!=="attr-remove"?e:e.oldValue==null&&e.newValue!=null?{...e,kind:"attr-add"}:e.oldValue!=null&&e.newValue==null?{...e,kind:"attr-remove"}:{...e,kind:"attr-set"}}var Xe=new WeakMap,Xt=0;function _(e){let n=Xe.get(e);return n||(n=`n${++Xt}`,Xe.set(e,n)),n}function N(e){return!!e&&typeof e=="object"&&typeof e.nodeType=="number"}function Ht(e){return e.kind==="add"||e.kind==="remove"}function Ge(e,n){let t=[],o=e.childNodes;if(!o)return t;for(let a of o)n&&n(a)||t.push(_(a));return t}function He(e,n,t){if(typeof n!="function"||!N(e)||e.nodeType!==1)return null;let o=null;try{o=n(e,t)}catch{o=null}return typeof o=="string"?`#form:${JSON.stringify(o)}`:null}function Q(e,n,t=null,o=null,a=null,i=null){if(!N(e))return null;let s=He(e,a,t);if(s!=null)return s;if(e.nodeType===3)return`#text:${JSON.stringify(i&&i.has(e)?i.get(e):e.data)}`;if(e.nodeType===8)return`#comment:${JSON.stringify(e.data)}`;if(e.nodeType!==1)return`#node:${e.nodeType}`;let d=[];if(e.attributes){for(let p of e.attributes)Jt(p.name,e,n,t)||d.push(`${p.name}=${JSON.stringify(p.value)}`);d.sort()}let f=[];if(e.childNodes)for(let p of e.childNodes){let w=He(p,a,t);if(w!=null){f.push(w);continue}Yt(p,o)||f.push(Q(p,n,t,o,a,i))}return`<${String(e.tagName||"").toLowerCase()}${d.length?` ${d.join(" ")}`:""}>${f.join("")}</>`}function ye(e,n=[]){if(!N(e))return n;if(e.nodeType===1){let t=e.getAttribute&&e.getAttribute("data-id");t&&n.push(t)}if(e.childNodes)for(let t of e.childNodes)ye(t,n);return n}function Jt(e,n,t,o=null){if(typeof t!="function")return!1;try{return!!t(e,n,o)}catch{return!1}}function Yt(e,n){if(typeof n!="function")return!1;try{return!!n(e)}catch{return!1}}function Ze(e,n){if(typeof n!="function")return[];let t=[],o=a=>{if(!N(a))return;let i=!1;try{i=!!n(a)}catch{i=!1}if(i&&t.push(I(a,"protected")),a.childNodes)for(let s of a.childNodes)o(s)};for(let a of e)o(a);return t}function I(e,n,t=null){let o=e;o&&o.nodeType!==1&&(o=o.parentElement);let a=o&&typeof o.closest=="function"?o.closest("[data-id]"):null;return!a&&t&&typeof t.closest=="function"&&(a=t.closest("[data-id]")),{id:(a?a.getAttribute("data-id"):null)||null,kind:n||null}}function Gt(e,n,t=null){return(e||[]).filter(N).map(o=>I(o,n,t))}function Z(e){return{replayable:!1,reason:e,steps:[]}}function ie(e,n){let t=[[n.afterAuthored,a=>a],[n.beforeAuthored,a=>a+1],[n.before,a=>a]],o=!1;for(let[a,i]of t){if(!N(a))continue;o=!0;let s=e.indexOf(_(a));if(s>=0)return i(s)}return o?null:e.length}function Qe(e,n={}){let t=n.isIgnored||null,o=n.ignoreAttribute||null,a=n.durableForm||null,i=je(e,{sameContent:ae}),s=[];for(let w of i){if(!Ht(w)){if(!N(w.target))return Z("non-dom-target");continue}if(!N(w.parent))return Z("non-dom-parent");let k=Array.from(w.nodes||[]).filter(N);if(k.length!==0){if(k.length!==Array.from(w.nodes||[]).length)return Z("non-dom-node");s.push({primitive:w,nodes:k,parent:w.parent})}}if(Zt(s))return Z("in-record-move");let d=Qt(s,t);if(!d)return Z("ambiguous-structure");let f=en(s,d),y=new Map(s.map(w=>[w.primitive,w])),p=[];for(let w of i){let k=y.get(w);if(!k){p.push(nn(w));continue}let A=f.get(k);if(A){A.at===k&&p.push(A.step);continue}p.push(tn(k,d.get(k),o,t,a))}return{replayable:!0,reason:null,steps:p}}function Zt(e){let n=new Map;for(let t of e){let o=t.primitive.record;if(!o)continue;let a=n.get(o);a||(a=new Map,n.set(o,a));for(let i of t.nodes){let s=a.get(i);s||(s=new Set,a.set(i,s)),s.add(t.primitive.kind)}}for(let t of n.values())for(let o of t.values())if(o.size>1)return!0;return!1}function Qt(e,n){let t=new Map;for(let a of e)t.has(a.parent)||t.set(a.parent,Ge(a.parent,n));let o=new Map;for(let a=e.length-1;a>=0;a--){let i=e[a],s=t.get(i.parent);if(!s)return null;let d=s.slice(),f;if(i.primitive.kind==="add"){let y=i.nodes.map(_);if(!y.every(p=>s.includes(p)))return null;f=s.filter(p=>!y.includes(p))}else{let y=ie(d,i.primitive);if(y==null)return null;f=d.slice(),f.splice(y,0,...i.nodes.map(_))}o.set(i,{before:f,after:d}),t.set(i.parent,f)}return o}function en(e,n){let t=new Map,o=new Map;e.forEach((i,s)=>{for(let d of i.nodes){let f=_(d);i.primitive.kind==="remove"?t.set(f,{item:i,index:s,node:d}):o.set(f,{item:i,index:s,node:d})}});let a=new Map;for(let[i,s]of t){let d=o.get(i);if(!d||d.index<s.index||s.item.nodes.length!==1||d.item.nodes.length!==1)continue;let f={type:"move",node:s.node,sourceParent:s.item.parent,destParent:d.item.parent,source:n.get(s.item),dest:n.get(d.item),sourceSlot:s.item.primitive,destSlot:d.item.primitive,targets:[I(s.node,"move")]};a.set(s.item,{step:f,at:d.item}),a.set(d.item,{step:f,at:d.item})}return a}function tn(e,n,t,o,a=null){let i=e.primitive.kind;return{type:i==="add"?"add":"restore",parent:e.parent,nodes:e.nodes,seqBefore:n.before,seqAfter:n.after,slot:e.primitive,content:e.nodes.map(s=>Q(s,t,e.parent,o,a)),ids:i==="remove"?e.nodes.flatMap(s=>ye(s)):[],targets:Gt(e.nodes,i==="add"?"added":"removed",e.parent)}}function nn(e){return{type:e.kind,primitive:e,targets:[I(e.target,e.name||e.prop||"text")]}}function et(e,n,t={}){if(!e||!e.replayable)return{ok:!1,code:"unsupported",targets:[]};let o={isIgnored:t.isIgnored||null,ignoreAttribute:t.ignoreAttribute||null,durableForm:t.durableForm||null,isProtected:t.isProtected||null,inScope:t.inScope||(()=>!0),liveNodesWithIds:t.liveNodesWithIds||(()=>[]),sim:new Map,planned:new Map,texts:new Map,operations:[]};for(let a=0;a<e.steps.length;a++){let i=e.steps[n==="undo"?e.steps.length-1-a:a],s=on(i,n,o);if(s)return{ok:!1,code:s.code,targets:s.targets}}return{ok:!0,operations:o.operations}}function X(e,n){return e.sim.has(n)||e.sim.set(n,Ge(n,e.isIgnored)),e.sim.get(n)}function H(e,n){for(let t=e;t;t=t.parentNode)if(n.planned.has(t))return n.planned.get(t);return n.inScope(e)}function tt(e,n){return n.liveNodesWithIds(e).filter(t=>H(t,n)).length>0}function b(e,n){return{code:e,targets:n||[]}}function ae(e,n){return e===n?!0:typeof e!="string"||typeof n!="string"?!1:e.replace(/\u00A0|&nbsp;/g," ")===n.replace(/\u00A0|&nbsp;/g," ")}function rn(e){return e.kind==="text"?e.target.data:e.kind==="value"?typeof e.read=="function"?e.read(e.target):e.target[e.prop||"value"]:e.target.getAttribute(e.name)}function on(e,n,t){let o=n==="undo";return e.type==="move"?dn(e,o,t):e.type==="add"?un(e,o,t):e.type==="restore"?sn(e,o,t):an(e,o,t)}function an(e,n,t){let o=e.primitive,a=n?o.newValue:o.oldValue,i=n?o.oldValue:o.newValue;if(!N(o.target))return b("unsupported",e.targets);if(!H(o.target,t))return b("target-detached",e.targets);if(o.kind==="value"&&typeof o.write=="function"&&typeof o.read!="function")return b("unsupported",e.targets);let s=rn(o);return(o.content===!0?!ae(s,a):s!==a)?b("content-changed",e.targets):(o.kind==="value"?t.operations.push({type:"value",target:o.target,prop:o.prop||"value",value:i,read:o.read||null,write:o.write||null,content:o.content===!0}):o.kind==="text"?(t.operations.push({type:"text",target:o.target,data:i}),t.texts.set(o.target,i)):t.operations.push({type:"attr",target:o.target,name:o.name,value:i??null}),null)}function un(e,n,t){if(!N(e.parent)||!H(e.parent,t))return b("target-detached",e.targets);if(n){if(!J(X(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);let d=Ze(e.nodes,t.isProtected);if(d.length>0)return b("protected",d);for(let f=0;f<e.nodes.length;f++)if(Q(e.nodes[f],t.ignoreAttribute,e.parent,t.isIgnored,t.durableForm)!==e.content[f])return b("content-changed",e.targets);for(let f of e.nodes)t.operations.push({type:"remove",node:f}),t.planned.set(f,!1);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!J(X(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);for(let d of e.nodes)if(d.parentNode)return b("structure-changed",e.targets);let o=e.nodes.flatMap(d=>ye(d));if(o.length>0&&tt(o,t))return b("duplicate-id",e.targets);let a=ie(e.seqBefore,e.slot);if(a==null)return b("unsupported",e.targets);let i=e.seqBefore.slice(),s=a;for(let d of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:d,beforeId:i[s]??null}),i=i.slice(0,s).concat(_(d),i.slice(s)),s+=1,t.planned.set(d,!0);return t.sim.set(e.parent,e.seqAfter.slice()),null}function sn(e,n,t){if(!N(e.parent)||!H(e.parent,t))return b("target-detached",e.targets);if(n){if(!J(X(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);for(let f=0;f<e.nodes.length;f++){let y=e.nodes[f];if(y.parentNode||Q(y,t.ignoreAttribute,e.parent,t.isIgnored,t.durableForm)!==e.content[f])return b("content-changed",e.targets)}if(e.ids.length>0&&tt(e.ids,t))return b("duplicate-id",e.targets);let i=ie(e.seqAfter,e.slot);if(i==null)return b("unsupported",e.targets);let s=e.seqAfter.slice(),d=i;for(let f of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:f,beforeId:s[d]??null}),s=s.slice(0,d).concat(_(f),s.slice(d)),d+=1,t.planned.set(f,!0);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!J(X(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);let o=Ze(e.nodes,t.isProtected);if(o.length>0)return b("protected",o);let a=i=>t.planned.get(i)===!0||typeof t.isIgnored=="function"&&t.isIgnored(i);for(let i=0;i<e.nodes.length;i++){let s=e.nodes[i];if(s.parentNode!==e.parent||Q(s,t.ignoreAttribute,e.parent,a,t.durableForm,t.texts)!==e.content[i])return b("content-changed",e.targets)}for(let i of e.nodes)t.operations.push({type:"remove",node:i}),t.planned.set(i,!1);return t.sim.set(e.parent,e.seqAfter.slice()),null}function ln(e,n){let t=e.indexOf(n);return t<0?null:e.slice(0,t).concat(e.slice(t+1))}function dn(e,n,t){let o=_(e.node),a=n?e.destParent:e.sourceParent,i=n?e.sourceParent:e.destParent,s=X(t,a),d=X(t,i),f=n?e.dest.after:e.source.before,y=n?e.source.after:e.dest.before,p=n?e.sourceSlot:e.destSlot;if(!N(a)||!N(i)||!H(a,t)||!H(i,t))return b("target-detached",e.targets);if(!J(s,f))return b("structure-changed",e.targets);let w=a===i?ln(d,o):d;if(!J(w,y))return b("structure-changed",e.targets);let k=ie(y,p);return k==null?b("unsupported",e.targets):(t.operations.push({type:"insert",parent:i,node:e.node,beforeId:y[k]??null}),t.planned.set(e.node,!0),n?(t.sim.set(i,e.source.before.slice()),a!==i&&t.sim.set(a,e.dest.before.slice())):(t.sim.set(i,e.dest.after.slice()),a!==i&&t.sim.set(a,e.source.after.slice())),null)}function J(e,n){if(!e||!n||e.length!==n.length)return!1;for(let t=0;t<e.length;t++)if(e[t]!==n[t])return!1;return!0}function cn(e,n){for(let t of e.childNodes||[])if(_(t)===n)return t;return null}function Je(e,n){let t=Math.min(e.length,n.length),o=0;for(;o<t&&e[o]===n[o];)o++;let a=0;for(;a<t-o&&e[e.length-1-a]===n[n.length-1-a];)a++;return n.length-a}function Ye(e,n){typeof e.write=="function"?e.write(e.target,n):e.target[e.prop]=n}function nt(e){let n=[];try{let t=null;for(let o of e)t=fn(o,n)||t;return{ok:!0,caret:t}}catch(t){for(let o=n.length-1;o>=0;o--)try{n[o]()}catch{}return{ok:!1,error:t}}}function fn(e,n){if(e.type==="attr"){let t=e.target.getAttribute(e.name);e.value==null?e.target.removeAttribute(e.name):e.target.setAttribute(e.name,e.value),n.push(()=>{t==null?e.target.removeAttribute(e.name):e.target.setAttribute(e.name,t)});return}if(e.type==="text"){let t=e.target.data;return e.target.data=e.data,n.push(()=>{e.target.data=t}),{node:e.target,offset:Je(t,e.data)}}if(e.type==="value"){let t=typeof e.read=="function"?e.read(e.target):e.target[e.prop],o=e.content?e.target.textContent:null;if(Ye(e,e.value),n.push(()=>Ye(e,t)),!e.content)return;let a=e.target.textContent;return a===o?void 0:{root:e.target,textOffset:Je(o,a)}}if(e.type==="insert"){let t=e.beforeId==null?null:cn(e.parent,e.beforeId);if(e.beforeId!=null&&!t)throw new Error("hyper-undo: replay anchor vanished");let o=e.node.parentNode,a=e.node.nextSibling;return e.parent.insertBefore(e.node,t),n.push(()=>{o?o.insertBefore(e.node,a&&a.parentNode===o?a:null):e.node.parentNode===e.parent&&e.parent.removeChild(e.node)}),e.node.nodeType!==3?void 0:{node:e.node,offset:e.node.data.length}}if(e.type==="remove"){let t=e.node.parentNode;if(!t)throw new Error("hyper-undo: replay target is no longer attached");let o=e.node.nextSibling,a=Array.prototype.indexOf.call(t.childNodes,e.node);return t.removeChild(e.node),n.push(()=>t.insertBefore(e.node,o)),e.node.nodeType!==3?void 0:{node:t,offset:a}}}var pn=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],mn=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],rt=pn.join(","),ot=new RegExp("^(?:"+mn.join("|")+")(?:-|$)");var Dn=Object.freeze({data:Object.freeze({tokens:["no-data"],bundles:["editor-ui"]}),save:Object.freeze({tokens:["no-save"],bundles:["editor-ui"]}),snapshot:Object.freeze({tokens:["no-snapshot"],bundles:["editor-ui"]}),watch:Object.freeze({tokens:["no-watch"],bundles:["editor-ui"]}),undo:Object.freeze({tokens:["no-undo"],bundles:["editor-ui"]}),history:Object.freeze({tokens:[],bundles:["editor-ui"]})}),gn=Object.freeze({"editor-ui":Object.freeze(["no-data","no-save","no-snapshot","no-watch","no-undo"])}),hn=Object.freeze(["no-save","no-snapshot","no-trigger-autosave","no-dirty","no-watch","no-undo","no-data","freeze","editor-ui","live"]);function ue(e,n){if(!e||e.nodeType!==1)return!1;let t=e.getAttribute?.("clay");return!!(t&&t.split(/\s+/).includes(n)||e.hasAttribute?.(n))}function we(e,n){if(ue(e,n))return!0;for(let[t,o]of Object.entries(gn))if(o.includes(n)&&ue(e,t))return!0;return!1}function it(e){return e==="clay"||hn.includes(e)}var yn=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function wn(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function be(e){let n=wn();if(n)try{if(!n(e).undoable)return!0}catch{}let t=e&&e.nodeType!==1?e.parentElement:e;if(t&&t.closest&&t.closest(rt))return!0;for(;t&&t.nodeType===1;){if(we(t,"no-undo")||we(t,"no-watch"))return!0;for(let o of yn)if(ue(t,o))return!0;t=t.parentElement}return!1}function bn(e){let n=e.target,t=n.cloneNode(!1),o=t;for(;n.parentElement;){n=n.parentElement;let a=n.cloneNode(!1);a.appendChild(t),t=a}return e.oldValue==null?o.removeAttribute(e.attributeName):o.setAttribute(e.attributeName,e.oldValue),be(o)}function ee(e,n,t){if(t?.type==="attributes"&&it(t.attributeName)){if(be(e)&&bn(t))return!0}else if(be(e))return!0;if(t&&t.type==="attributes"){if(t.attributeName&&ot.test(t.attributeName.toLowerCase()))return!0;if(n)try{if(n(t.attributeName,t.target))return!0}catch{}}return!1}function at(){let e=new Map;function n(a,i){let s=e.get(a);return s||(s=new Set,e.set(a,s)),s.add(i),()=>t(a,i)}function t(a,i){let s=e.get(a);s&&s.delete(i)}function o(a,i){let s=e.get(a);if(s)for(let d of Array.from(s))try{d(i)}catch(f){console.error("[hyper-undo] listener threw",f)}}return{on:n,off:t,emit:o}}var kn=/[.?!]["'\u201D\u2019)\]]*$/,vn=new Set(["insertParagraph","insertLineBreak"]),En=new Set(["insertFromPaste","insertFromDrop","insertFromYank","insertReplacementText","deleteByCut","deleteByDrag"]),An=new Set(["Enter","Backspace","Delete"]),Sn=new Set(["ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Home","End","PageUp","PageDown"]);function ke(e){return kn.test(e)}function ut(e,{sentence:n,endStep:t,stepOpen:o=()=>!0,composing:a}){let i=e.ownerDocument&&e.ownerDocument.defaultView;if(!i)return()=>{};let s=m=>!!m&&typeof m.nodeType=="number"&&(m===e||e.contains(m)),d=!1,f=!1,y=!1,p={kind:"other",host:null},w=m=>En.has(m)||m.startsWith("format")?"alone":m.startsWith("delete")?"delete":m.startsWith("insert")?"insert":"other",k=m=>{if(!m)return null;let T=m.nodeType===1?m:m.parentElement;return T&&T.closest&&T.closest('[contenteditable]:not([contenteditable="false"])')||m},A=()=>{d&&(d=!1,t())},S=m=>{if(!n||m.isComposing||!s(m.target))return;let T=m.inputType||"",V=w(T),Y=k(m.target),te=i.getSelection&&i.getSelection(),G=!te||te.isCollapsed,se=f;f=!1;let U=y;if(y=!1,(V==="alone"||!U&&V!==p.kind&&V!=="other"&&p.kind!=="other"||!U&&(p.host!==null&&Y!==p.host||!G||se))&&t(),p={kind:V,host:Y},V==="alone"){d=!0,setTimeout(A,0);return}let W=T==="insertText"&&(m.data===" "||m.data==="\xA0");!W&&!vn.has(T)||W&&!o()||ke(Nn(i))&&(d=!0,setTimeout(A,0))},R=m=>{s(m.target)&&A()},C=m=>{s(m.target)&&a(!0)},D=m=>{s(m.target)&&a(!1)},K=m=>{s(m.target)&&(f=!0)},B=m=>{if(!s(m.target))return;if(Sn.has(m.key)){f=!0;return}if(!n||m.isComposing||m.keyCode===229||m.ctrlKey||m.metaKey||!m.key||m.key.length!==1&&!An.has(m.key))return;let T=i.getSelection&&i.getSelection(),V=p.host!==null&&k(m.target)!==p.host;!f&&!V&&(!T||T.isCollapsed)||(t(),f=!1,y=!0,setTimeout(()=>{y=!1},0))};return e.addEventListener("beforeinput",S,!0),e.addEventListener("input",R,!0),e.addEventListener("compositionstart",C,!0),e.addEventListener("compositionend",D,!0),e.addEventListener("pointerdown",K,!0),e.addEventListener("keydown",B,!0),()=>{e.removeEventListener("beforeinput",S,!0),e.removeEventListener("input",R,!0),e.removeEventListener("compositionstart",C,!0),e.removeEventListener("compositionend",D,!0),e.removeEventListener("pointerdown",K,!0),e.removeEventListener("keydown",B,!0)}}function Nn(e){let n=e.getSelection&&e.getSelection();if(!n||!n.isCollapsed||!n.anchorNode)return"";let t=n.anchorNode,o=t.nodeType===1?t:t.parentElement,a=o&&o.closest('[contenteditable]:not([contenteditable="false"])');if(!a)return"";let i=a.ownerDocument.createRange();return i.setStart(a,0),i.setEnd(t,n.anchorOffset),i.toString().replace(/\u200B/g,"").slice(-8)}var Tn={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",recording:"automatic",ignoreAttribute:null,isProtected:null,ignoreNode:null,durableForm:null,typingBreak:null,refused:null,debug:!1};function ve(e){let n={...Tn,...e};if(!n.scope)throw new Error("hyper-undo: scope is required");let t=at(),o=[],a=[],i=`u${Math.random().toString(36).slice(2,8)}`,s=0,d=null,f=[],y=null,p=null,w=0,k=!1,A=n.recording==="commands"?"commands":"automatic",S=null,R=!1,C=new WeakMap,D=!1,K=!1,B=null;function m(...r){n.debug&&console.log("[hyper-undo]",...r)}function T(){k||(k=!0,p=V(F),p.observe(n.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),B=ut(n.scope,{sentence:n.typingBreak==="sentence",stepOpen:()=>(w===0&&p&&F(p.takeRecords()),f.length>0),endStep:z,composing:yt}),m("started"))}function V(r){let u=typeof document<"u"&&n.scope===document.body,l=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return u&&l&&typeof l.createObserver=="function"?(m("sourcing records from the platform Mutation hub (shared observer)"),l.createObserver(r)):new MutationObserver(r)}function Y(){k&&(k=!1,p&&(p.disconnect(),p=null),B&&(B(),B=null),D=!1,q(),o.length=0,a.length=0,f=[],d=null,S=null,R=!1,w=0,C=new WeakMap,m("stopped"))}function te(){let r=n.scope;return!!(r&&r.nodeType===1&&r.isConnected===!1)}function G(r,u){try{let l=u(r);return typeof l=="string"?l:null}catch{return null}}function se(r,{read:u,write:l,prop:c="content"}={}){if(!r||typeof u!="function"||typeof l!="function")return()=>{};let h={prop:c,read:u,write:l,baseline:G(r,u)};return C.set(r,h),()=>{C.get(r)===h&&C.delete(r)}}function U(r){for(let u=r;u;u=u.parentNode)if(C.has(u))return u;return null}function W(r){let u=U(r.target);return!!u&&!(r.type==="attributes"&&r.target===u)}function L(r){let u=new Set;for(let l of r)W(l)&&u.add(U(l.target));return u}function le(r){let u=[];for(let l of r){let c=C.get(l),h=G(l,c.read);if(h!=null){if(ae(h,c.baseline)){c.baseline=h;continue}c.baseline!=null&&u.push({kind:"value",target:l,prop:c.prop,oldValue:c.baseline,newValue:h,read:c.read,write:c.write,content:!0}),c.baseline=h}}return u}function j(r){for(let u of r){let l=C.get(u);l.baseline=G(u,l.read)}}function Te(r){let u=L(r);!K&&f.some(l=>l.kind==="value"?u.has(l.target):r.some(c=>c.target===l.target&&(c.type==="characterData"?l.kind==="text":c.type==="attributes"&&c.attributeName===l.name)))&&(K=!0),j(u)}function F(r){Ce(r,!1)}function Ce(r,u){if(S){for(let c of r)S.records.push(c);return}if(A==="commands"){j(L(r));return}if(w>0){Te(r);return}let l=de(gt(r),u);for(let c of le(L(r)))l.push(c);if(l.length>0){for(let c of l)f.push(c);ce()}}function gt(r){return typeof n.ignoreNode!="function"?r:r.filter(u=>{if(u.type!=="childList")return!0;let l=[...u.addedNodes,...u.removedNodes];return l.length===0?!0:!l.every(c=>ht(c))})}function ht(r){try{return!!n.ignoreNode(r)}catch{return!1}}function de(r,u=!1){let l=_t(r),c=r.filter(h=>(ge(h.target)||l(h.target)||u&&ze(h.target))&&!W(h)&&!ee(h.target,n.ignoreAttribute,h));return We(c,h=>ee(h,n.ignoreAttribute))}function yt(r){D=!!r,!D&&f.length>0&&ce()}function ce(){q(),y=setTimeout(Ie,n.idleWindowMs)}function Ie(){if(y=null,D){y=setTimeout(Ie,n.idleWindowMs);return}fe()}function q(){y!=null&&(clearTimeout(y),y=null)}function fe(){if(y=null,f.length===0)return;let r=f;f=[],ne(n.idleLabel,r)}function ne(r,u){let l=Qe(u,{isIgnored:re,ignoreAttribute:Ve,durableForm:n.durableForm});if(l.steps.length===0)return m("commit",r,"with nothing to record"),t.emit("commit"),null;let c={id:`${i}-${++s}`,label:r,timestamp:Date.now(),primitives:u,certificate:l};for(o.push(c),a.length=0,d=null;o.length>n.maxHistory;)o.shift();return m("commit",r,"primitives:",u.length),t.emit("commit"),c}function re(r){return ee(r,n.ignoreAttribute)}function Ve(r,u,l=null){if(typeof n.ignoreAttribute!="function")return!1;try{return!!n.ignoreAttribute(r,u,l)}catch{return!1}}function wt(r){if(typeof n.isProtected!="function")return!1;try{return!!n.isProtected(r)}catch{return!1}}function bt(r,u){if(!k)throw new Error("hyper-undo: scope not started");if(A==="commands")return xe(r,u,{honorPause:!0,receipt:!1});F(p.takeRecords()),z();let l=w===0?Pe():null,c=u();if(c&&typeof c.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(Ce(p.takeRecords(),!0),l!=null&&Re(l),f.length===0){t.emit("commit");return}q();let h=f;f=[],ne(r,h)}function kt(r){if(!k)throw new Error("hyper-undo: scope not started");if(!p)return;if(w>1){j(L(p.takeRecords()));return}z();let u=p.takeRecords(),l=de(u,!0);for(let c of le(L(u)))l.push(c);l.length!==0&&(q(),ne(r,l))}function vt(){p&&j(L(p.takeRecords()))}function pe(){let r=n.scope;return r&&r.ownerDocument||(typeof document<"u"?document:null)}function oe(){let r=pe();return!r||!r.head||typeof r.head.querySelector!="function"?null:r.head.querySelector("title")}function Pe(){let r=oe();return r?r.textContent:null}function Re(r){let u=oe();if(!u||r==null)return;let l=u.textContent;l!==r&&_e({kind:"value",target:u,prop:"textContent",oldValue:r,newValue:l})}function Et(r){if(!r)return null;let u=pe();if(!u||r.target!==u||r.prop!=="title")return r;let l=oe();return l?{kind:"value",target:l,prop:"textContent",oldValue:r.oldValue,newValue:r.newValue}:null}function At(){if(typeof window>"u")return!1;let r=window.clay?.effect||window.hyperclay?.effect;return!!(r&&r.running)}function St(){p&&F(p.takeRecords()),z(),S={records:[],primitives:[]}}function Nt(){let r=S;if(S=null,!r)return[];let u=de(r.records,!0);for(let l of le(L(r.records)))u.push(l);for(let l of r.primitives)u.push(l);return u}function xe(r,u,{honorPause:l,receipt:c}){if(typeof u!="function")throw new Error("hyper-undo: command() needs a synchronous function");if(S)throw new Error("hyper-undo: command() cannot nest inside another command");if(R)throw new Error("hyper-undo: command() cannot run during undo or redo");if(At())throw new Error("hyper-undo: command() cannot run inside a derived-effect context");if(u.constructor&&u.constructor.name==="AsyncFunction")throw new Error("hyper-undo: command() fn must be synchronous; it was declared async");let h=l&&w>0,v=h?null:Pe();St();let x=[],E=null,P;try{try{P=u()}catch($){E=$}if(p&&F(p.takeRecords()),P&&typeof P.then=="function"){try{P.then(void 0,()=>{})}catch{}E||(E=new Error("hyper-undo: command() fn must be synchronous; returned a Promise"))}h||Re(v)}finally{x=Nt()}let O=h||x.length===0?null:ne(r,x);if(E)throw E;return c?O?{id:O.id,label:O.label}:null:O}function Tt(r,u){if(!k)throw new Error("hyper-undo: scope not started");return xe(r,u,{honorPause:!1,receipt:!0})}function Ct(r){if(r!=="automatic"&&r!=="commands")throw new Error("hyper-undo: setRecording() takes 'automatic' or 'commands'");if(S)throw new Error("hyper-undo: setRecording() cannot change while a command is running");return A===r||(z(),A=r,m("recording",r)),A}function Oe(r){if(!k||!r||!S&&(w>0||A==="commands"))return;let u=Et(r);u&&_e(u)}function _e(r){if(S){S.primitives.push(r);return}f.push(r),ce()}function It(r,{prop:u="value",oldValue:l,newValue:c,read:h,write:v}={}){!r||l===c||Oe({kind:"value",target:r,prop:u,oldValue:l,newValue:c,read:h,write:v})}function z(){w===0&&p&&F(p.takeRecords()),f.length!==0&&(q(),fe())}function Le(){w===0&&p&&F(p.takeRecords()),w++,m("paused",w)}function Me(){w!==0&&(w--,w===0&&p&&(Te(p.takeRecords()),K&&(K=!1,q(),fe())),m("resumed",w))}function De(r,u){if(te())return Y(),{ok:!1,code:"disconnected"};if(typeof n.holdReplay=="function"&&n.holdReplay())return{ok:!1,code:"held"};z();let l=r==="undo"?o:a,c=r==="undo"?a:o,h=[],v=l.at(-1);if(!v)return{ok:!1,code:"empty"};if(u&&u!==v.id){let E=Be(u)||v;return me(E,r,"stale-entry")}let x=n.refused==="skip"&&!u;for(;v;){let E;R=!0;try{E=et(v.certificate,r,{isIgnored:re,ignoreAttribute:Ve,durableForm:n.durableForm,isProtected:wt,inScope:Lt,liveNodesWithIds:Mt})}finally{R=!1}if(!E.ok){if(!x)return me(v,r,E.code,E.targets);Ke(v,r,E.code,E.targets,h),v=l.at(-1);continue}Le();let P;R=!0;try{P=nt(E.operations)}finally{R=!1,Me()}if(!P.ok){if(m(`${r} failed and was rolled back`,P.error),!x)return me(v,r,"replay-error");Ke(v,r,"replay-error",[],h),v=l.at(-1);continue}j(new Set(E.operations.filter($=>$.type==="value"&&C.has($.target)).map($=>$.target))),l.pop(),c.push(v),d=null;let O={ok:!0,entryId:v.id,label:v.label,direction:r};return h.length&&(O.skipped=h),P.caret&&(O.caret=P.caret),m(r,v.label),t.emit(r,O),O}return{ok:!1,code:"empty",direction:r,skipped:h}}function Ke(r,u,l,c,h){d={entryId:r.id,direction:u,code:l,targets:c};let v=Fe(r.id);(u==="undo"?o:a).pop(),d=null;let E={entryId:r.id,label:r.label,direction:u,code:l,targets:c,description:v};m("skipped",u,l),t.emit("skipped",E),h.push(E)}function Vt(r){return De("undo",r&&r.entryId)}function Pt(r){return De("redo",r&&r.entryId)}function Be(r){return o.find(u=>u.id===r)||a.find(u=>u.id===r)||null}function me(r,u,l,c=[]){return d={entryId:r.id,direction:u,code:l,targets:c},m("refused",u,l),t.emit("refused",{entryId:r.id,label:r.label,direction:u,code:l,targets:c}),{ok:!1,code:l,entryId:r.id,label:r.label,direction:u}}function Rt(r){if(!d||d.entryId!==r)return{ok:!1,code:"not-refused",entryId:r};let u=d.direction,l=u==="undo"?o:a,c=l.at(-1);return!c||c.id!==r?{ok:!1,code:"stale-entry",entryId:r}:(l.pop(),d=null,m("discarded",c.label),t.emit("discard",{entryId:r,label:c.label,direction:u}),{ok:!0,entryId:r,direction:u})}function Fe(r){let u=Be(r);if(!u||!u.certificate||!u.certificate.replayable)return null;let l=d&&d.entryId===u.id?{direction:d.direction,code:d.code,targets:(d.targets||[]).map(c=>({id:c.id??null,kind:c.kind??null}))}:null;return{id:u.id,label:u.label,timestamp:u.timestamp,refused:l,steps:u.certificate.steps.map(Ot)}}function xt(r){return typeof r!="string"?null:r.replace(/#form:("(?:[^"\\]|\\.)*")/g,(u,l)=>{try{return JSON.parse(l)}catch{return""}})}function Ot(r){if(r.type==="move")return{kind:"move",node:{id:I(r.node).id},from:{id:I(r.sourceParent).id},to:{id:I(r.destParent).id}};if(r.type==="add"||r.type==="restore")return{kind:r.type==="add"?"add":"remove",parent:{id:I(r.parent).id},nodes:r.nodes.map((v,x)=>({id:I(v).id,html:xt(r.content[x])}))};let u=r.primitive,l={id:I(u.target).id},c=u.oldValue??null,h=u.newValue??null;return r.type==="value"?{kind:"value",target:l,prop:u.prop??"value",before:c,after:h}:r.type==="text"?{kind:"text",target:l,before:c,after:h}:r.type==="attr-set"||r.type==="attr-add"||r.type==="attr-remove"?{kind:"attribute",target:l,name:u.name??null,before:c,after:h}:{kind:r.type,target:l,before:c,after:h}}function _t(r){let u=new Set,l=!0;for(;l;){l=!1;for(let c of r)if(!(c.type!=="childList"||!c.removedNodes?.length)&&!W(c)&&!(!ge(c.target)&&!qe(c.target,u))&&!ee(c.target,n.ignoreAttribute,c))for(let h of c.removedNodes)u.has(h)||(u.add(h),l=!0)}return c=>!ge(c)&&qe(c,u)}function qe(r,u){for(let l=r;l;l=l.parentNode)if(u.has(l))return!0;return!1}function ge(r){let u=n.scope;return!u||!r||typeof r.nodeType!="number"?!1:r===u||typeof u.contains=="function"&&u.contains(r)}function ze(r){let u=pe(),l=u&&u.head;if(!l||!r)return!1;let c=r.nodeType===1?r:r.parentElement;for(;c&&c!==l;c=c.parentElement)if(c.localName==="title")return!1;return c===l}function Lt(r){let u=n.scope;return!r||!u?!1:r===u||oe()===r||ze(r)?!0:typeof u.contains=="function"?u.contains(r):!1}function Mt(r){let u=n.scope;if(!r||r.length===0||typeof u?.querySelectorAll!="function")return[];let l=new Set(r),c=[];for(let h of u.querySelectorAll("[data-id]"))l.has(h.getAttribute("data-id"))&&(re(h)||c.push(h));return c}function Dt(){p&&j(L(p.takeRecords())),o.length=0,a.length=0,f=[],d=null,q(),m("cleared"),t.emit("clear")}return{_config:n,_tracksContentAt:r=>A!=="commands"&&!!U(r),_recordsAt:r=>A!=="commands"&&!re(r),start:T,stop:Y,commit:bt,commitCaptured:kt,discardCaptured:vt,flush:z,command:Tt,setRecording:Ct,record:Oe,recordValue:It,trackContent:se,undo:Vt,redo:Pt,clear:Dt,discard:Rt,describe:Fe,pause:Le,resume:Me,on:t.on,off:t.off,get canUndo(){return o.length>0},get canRedo(){return a.length>0},get isPaused(){return w>0},get history(){return o.map(r=>{let u={id:r.id,label:r.label,timestamp:r.timestamp};return d&&d.entryId===r.id&&(u.refused={direction:d.direction,code:d.code,targets:d.targets}),u})},get future(){return a.map(r=>{let u={id:r.id,label:r.label,timestamp:r.timestamp};return d&&d.entryId===r.id&&(u.refused={direction:d.direction,code:d.code,targets:d.targets}),u}).reverse()},get scope(){return n.scope}}}var Cn=new Set(["checkbox","radio","button","submit","reset","image","file","range","color"]);function dt(e){let n=typeof e.composedPath=="function"?e.composedPath():[];return n.length>0?n:Vn(e.target)}function In(e){for(let n of dt(e)){if(!n||n.nodeType!==1)continue;let t=String(n.tagName||"").toUpperCase();if(t==="TEXTAREA"||t==="SELECT"||t==="INPUT"&&!Cn.has(String(n.type||n.getAttribute?.("type")||"text").toLowerCase()))return!0}return!1}function st(e){for(let n of dt(e)){if(!n||n.nodeType!==1)continue;if(n.isContentEditable===!0)return!0;let t=n.getAttribute&&n.getAttribute("contenteditable");if(t!=null&&String(t).toLowerCase()!=="false")return!0}return!1}function Ee(e,n){if(!e||!e.closest||!Array.isArray(n))return!1;for(let t of n)try{if(e.closest(t))return!0}catch{}return!1}function lt(e,n){if(In(e))return!0;let t=n._config,o=e.target;return t.editableKeys!=="page"?st(e)||Ee(o,t.shadowKeydownIn):Ee(o,t.ownKeysIn)||n._tracksContentAt(o)?!1:Ee(o,t.shadowKeydownIn)?!0:st(e)&&!n._recordsAt(o)}function Vn(e){let n=[];for(let t=e;t;t=t.parentNode||(t.host??null))n.push(t);return n}function ct(e,n){let t=i=>{let s=e.scope;if(s&&s.nodeType===1&&s.isConnected===!1){n&&n();return}if(i.defaultPrevented||i.isComposing||i.keyCode===229||lt(i,e)||!(i.metaKey||i.ctrlKey))return;let f=(i.key||"").toLowerCase();if(f==="z"&&!i.shiftKey){i.preventDefault(),i.stopPropagation(),e.undo();return}if(f==="z"&&i.shiftKey){i.preventDefault(),i.stopPropagation(),e.redo();return}if(f==="y"&&!i.shiftKey){i.preventDefault(),i.stopPropagation(),e.redo();return}},o=i=>{if(i.inputType!=="historyUndo"&&i.inputType!=="historyRedo")return;let s=e.scope;s&&s.nodeType===1&&s.isConnected===!1||i.defaultPrevented||i.isComposing||lt(i,e)||(i.preventDefault(),i.stopPropagation(),i.inputType==="historyRedo"?e.redo():e.undo())},a=e._config.editableKeys==="page";return window.addEventListener("keydown",t,!0),a&&window.addEventListener("beforeinput",o,!0),()=>{window.removeEventListener("keydown",t,!0),a&&window.removeEventListener("beforeinput",o,!0)}}var Ae=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror",".overtype-input","[data-richclay-active]",".ck-editor__editable","[data-lexical-editor]","[data-slate-editor]","trix-editor",".mce-content-body"],g=null,Se=null,ft=null,M=null;function pt(e,n){if(M){if(M.inst===e)return M.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let t=ct(e,n);return M={inst:e,cleanup:t},t}function mt(e){M&&M.inst===e&&(M.cleanup(),M=null)}var Ne={defaults:{shadowKeydownIn:Ae},start(e={}){let n=e.scope||(typeof document<"u"?document.body:null);if(!n)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(g){if(Se===n)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),g;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let t={...e,scope:n,shadowKeydownIn:e.shadowKeydownIn||Ae};return g=ve(t),g.start(),Se=n,e.bindKeys!==!1&&(ft=pt(g,()=>Ne.stop())),g},stop(){g&&(mt(g),ft=null,g.stop(),g=null,Se=null)},create(e={}){let n=e.scope||(typeof document<"u"?document.body:null);if(!n)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let t={...e,scope:n,shadowKeydownIn:e.shadowKeydownIn||Ae},o=ve(t),a=null,i={_config:o._config,commit:o.commit,command:o.command,setRecording:o.setRecording,commitCaptured:o.commitCaptured,discardCaptured:o.discardCaptured,flush:o.flush,record:o.record,recordValue:o.recordValue,trackContent:o.trackContent,undo:o.undo,redo:o.redo,clear:o.clear,discard:o.discard,describe:o.describe,pause:o.pause,resume:o.resume,on:o.on,off:o.off,get canUndo(){return o.canUndo},get canRedo(){return o.canRedo},get isPaused(){return o.isPaused},get history(){return o.history},get future(){return o.future},get scope(){return o.scope},start(){e.bindKeys&&(a=pt(o,()=>i.stop())),o.start()},stop(){mt(o),a=null,o.stop()}};return i},commit(e,n){return g?g.commit(e,n):void 0},command(e,n){return g?g.command(e,n):void 0},setRecording(e){return g?g.setRecording(e):void 0},commitCaptured(e){return g?g.commitCaptured(e):void 0},discardCaptured(){return g?g.discardCaptured():void 0},record(e){return g?g.record(e):void 0},recordValue(e,n){return g?g.recordValue(e,n):void 0},trackContent(e,n){return g?g.trackContent(e,n):()=>{}},undo(e){return g?g.undo(e):void 0},redo(e){return g?g.redo(e):void 0},clear(){return g?g.clear():void 0},discard(e){return g?g.discard(e):void 0},describe(e){return g?g.describe(e):void 0},pause(){return g?g.pause():void 0},resume(){return g?g.resume():void 0},flush(){return g?g.flush():void 0},on(e,n){return g?g.on(e,n):void 0},off(e,n){return g?g.off(e,n):void 0},get canUndo(){return g?g.canUndo:!1},get canRedo(){return g?g.canRedo:!1},get history(){return g?g.history:[]},get future(){return g?g.future:[]},get isPaused(){return g?g.isPaused:!1}};var Pn=Ne;return $t(Rn);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-undo",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "DOM-state undo/redo via MutationObserver inverse-op replay. Standalone or paired with hyperclayjs.",
5
5
  "keywords": [
6
6
  "undo",
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createScope } from './scope.js'
2
2
  import { installKeys } from './keys.js'
3
+ import { endsSentence } from './typing.js'
3
4
 
4
5
  const DEFAULT_SHADOW_LIST = [
5
6
  '.CodeMirror',
@@ -21,6 +22,14 @@ const DEFAULT_SHADOW_LIST = [
21
22
  // in richclay/src/richclay.js, which says it marks the region so hyper-undo
22
23
  // "defers to Squire instead of fighting it".
23
24
  '[data-richclay-active]',
25
+ // Embedded third-party editors keep their own history: the page stack never
26
+ // records what is typed in them, so a page-mode Cmd+Z there would revert some
27
+ // other field instead of the editor's own last change.
28
+ '.ck-editor__editable',
29
+ '[data-lexical-editor]',
30
+ '[data-slate-editor]',
31
+ 'trix-editor',
32
+ '.mce-content-body',
24
33
  ]
25
34
 
26
35
  let singleton = null // the Scope instance returned by createScope
@@ -113,6 +122,7 @@ const undo = {
113
122
  flush: inst.flush,
114
123
  record: inst.record,
115
124
  recordValue: inst.recordValue,
125
+ trackContent: inst.trackContent,
116
126
  undo: inst.undo,
117
127
  redo: inst.redo,
118
128
  clear: inst.clear,
@@ -152,6 +162,7 @@ const undo = {
152
162
  discardCaptured() { return singleton ? singleton.discardCaptured() : undefined },
153
163
  record(p) { return singleton ? singleton.record(p) : undefined },
154
164
  recordValue(t, o) { return singleton ? singleton.recordValue(t, o) : undefined },
165
+ trackContent(root, o) { return singleton ? singleton.trackContent(root, o) : () => {} },
155
166
  undo(opts) { return singleton ? singleton.undo(opts) : undefined },
156
167
  redo(opts) { return singleton ? singleton.redo(opts) : undefined },
157
168
  clear() { return singleton ? singleton.clear() : undefined },
@@ -169,5 +180,5 @@ const undo = {
169
180
  get isPaused() { return singleton ? singleton.isPaused : false },
170
181
  }
171
182
 
172
- export { undo }
183
+ export { undo, endsSentence }
173
184
  export default undo
package/src/keys.js CHANGED
@@ -2,16 +2,26 @@
2
2
  // subtree)? The browser's own undo belongs there, not the page stack's.
3
3
  const NO_TEXT_INPUTS = new Set(['checkbox', 'radio', 'button', 'submit', 'reset', 'image', 'file', 'range', 'color'])
4
4
 
5
- function ownsNativeUndo(event) {
5
+ function pathOf(event) {
6
6
  const path = typeof event.composedPath === 'function' ? event.composedPath() : []
7
- const nodes = path.length > 0 ? path : chainFromTarget(event.target)
8
- for (const node of nodes) {
7
+ return path.length > 0 ? path : chainFromTarget(event.target)
8
+ }
9
+
10
+ function inFormControl(event) {
11
+ for (const node of pathOf(event)) {
9
12
  if (!node || node.nodeType !== 1) continue
10
13
  const tag = String(node.tagName || '').toUpperCase()
11
14
  if (tag === 'TEXTAREA' || tag === 'SELECT') return true
12
15
  // An input that takes no typed text has no native undo of its own: a checkbox,
13
16
  // a radio or a button leaves the key to the page stack.
14
17
  if (tag === 'INPUT' && !NO_TEXT_INPUTS.has(String(node.type || node.getAttribute?.('type') || 'text').toLowerCase())) return true
18
+ }
19
+ return false
20
+ }
21
+
22
+ function inEditable(event) {
23
+ for (const node of pathOf(event)) {
24
+ if (!node || node.nodeType !== 1) continue
15
25
  if (node.isContentEditable === true) return true
16
26
  // jsdom (and older engines) do not resolve isContentEditable, so read the
17
27
  // declaration itself; an unset or explicit "false" is not editable.
@@ -21,6 +31,28 @@ function ownsNativeUndo(event) {
21
31
  return false
22
32
  }
23
33
 
34
+ function inAny(target, selectors) {
35
+ if (!target || !target.closest || !Array.isArray(selectors)) return false
36
+ for (const selector of selectors) {
37
+ try { if (target.closest(selector)) return true }
38
+ catch (_) { /* invalid selector, skip */ }
39
+ }
40
+ return false
41
+ }
42
+
43
+ // Does this history key or history input belong to someone other than the page
44
+ // stack? "page" owns editable content the scope records; a form control, an
45
+ // authored no-undo region or another in-page editor keeps its own undo.
46
+ function standsDown(event, scope) {
47
+ if (inFormControl(event)) return true
48
+ const config = scope._config
49
+ const target = event.target
50
+ if (config.editableKeys !== 'page') return inEditable(event) || inAny(target, config.shadowKeydownIn)
51
+ if (inAny(target, config.ownKeysIn) || scope._tracksContentAt(target)) return false
52
+ if (inAny(target, config.shadowKeydownIn)) return true
53
+ return inEditable(event) && !scope._recordsAt(target)
54
+ }
55
+
24
56
  function chainFromTarget(target) {
25
57
  const out = []
26
58
  for (let node = target; node; node = node.parentNode || (node.host ?? null)) out.push(node)
@@ -44,19 +76,7 @@ export function installKeys(scope, onDetached) {
44
76
  if (event.defaultPrevented) return
45
77
  // The IME owns keys while it composes.
46
78
  if (event.isComposing || event.keyCode === 229) return
47
- // A plain form control keeps the browser's own text undo. Only the unsent
48
- // control changes; Save/Enter later commits that text as a page command.
49
- if (ownsNativeUndo(event)) return
50
-
51
- // Bypass for in-page editors (CodeMirror, Monaco, etc.)
52
- const target = event.target
53
- const shadow = scope._config.shadowKeydownIn
54
- if (target && target.closest && Array.isArray(shadow)) {
55
- for (const selector of shadow) {
56
- try { if (target.closest(selector)) return }
57
- catch (_) { /* invalid selector, skip */ }
58
- }
59
- }
79
+ if (standsDown(event, scope)) return
60
80
 
61
81
  const mod = event.metaKey || event.ctrlKey
62
82
  if (!mod) return
@@ -80,6 +100,24 @@ export function installKeys(scope, onDetached) {
80
100
  }
81
101
  }
82
102
 
103
+ // Chrome delivers the Edit menu's Undo as a cancellable beforeinput event.
104
+ // Owned by the same rule as the key, it moves the page stack instead.
105
+ const onHistoryInput = (event) => {
106
+ if (event.inputType !== 'historyUndo' && event.inputType !== 'historyRedo') return
107
+ const scopeEl = scope.scope
108
+ if (scopeEl && scopeEl.nodeType === 1 && scopeEl.isConnected === false) return
109
+ if (event.defaultPrevented || event.isComposing) return
110
+ if (standsDown(event, scope)) return
111
+ event.preventDefault(); event.stopPropagation()
112
+ if (event.inputType === 'historyRedo') scope.redo()
113
+ else scope.undo()
114
+ }
115
+
116
+ const ownsInput = scope._config.editableKeys === 'page'
83
117
  window.addEventListener('keydown', handler, true)
84
- return () => window.removeEventListener('keydown', handler, true)
118
+ if (ownsInput) window.addEventListener('beforeinput', onHistoryInput, true)
119
+ return () => {
120
+ window.removeEventListener('keydown', handler, true)
121
+ if (ownsInput) window.removeEventListener('beforeinput', onHistoryInput, true)
122
+ }
85
123
  }
package/src/primitives.js CHANGED
@@ -124,7 +124,7 @@ export function recordsToPrimitives(records, ignoreNode = null) {
124
124
  // more than once. Keep ONE primitive per (target, field) carrying the first old
125
125
  // value and the final new value, and drop the ones whose endpoints are equal:
126
126
  // undo must walk back to the value before the entry, not to an intermediate one.
127
- export function normalizePrimitives(primitives) {
127
+ export function normalizePrimitives(primitives, { sameContent = (a, b) => a === b } = {}) {
128
128
  const out = []
129
129
  const slots = new Map() // target → Map(field → index in out)
130
130
 
@@ -148,6 +148,7 @@ export function normalizePrimitives(primitives) {
148
148
  for (const p of out) {
149
149
  if (scalarField(p) == null) { normalized.push(p); continue }
150
150
  if (p.oldValue === p.newValue) continue
151
+ if (p.kind === 'value' && p.content === true && sameContent(p.oldValue, p.newValue)) continue
151
152
  normalized.push(reclassifyScalar(p))
152
153
  }
153
154
  return normalized
@@ -53,9 +53,18 @@ function childIdentities(parent, isIgnored) {
53
53
  // `host` is the element a removed subtree belongs under. A detached node has no
54
54
  // ancestors left, so a predicate that reads them (is this attribute painted by an
55
55
  // app above it?) receives the host and answers as it did while attached.
56
- export function durableProjection(node, ignoredAttribute, host = null, ignoredNode = null) {
56
+ function ownForm(node, form, host) {
57
+ if (typeof form !== 'function' || !isNode(node) || node.nodeType !== 1) return null
58
+ let own = null
59
+ try { own = form(node, host) } catch (_) { own = null }
60
+ return typeof own === 'string' ? `#form:${JSON.stringify(own)}` : null
61
+ }
62
+
63
+ export function durableProjection(node, ignoredAttribute, host = null, ignoredNode = null, form = null, texts = null) {
57
64
  if (!isNode(node)) return null
58
- if (node.nodeType === 3) return `#text:${JSON.stringify(node.data)}`
65
+ const own = ownForm(node, form, host)
66
+ if (own != null) return own
67
+ if (node.nodeType === 3) return `#text:${JSON.stringify(texts && texts.has(node) ? texts.get(node) : node.data)}`
59
68
  if (node.nodeType === 8) return `#comment:${JSON.stringify(node.data)}`
60
69
  if (node.nodeType !== 1) return `#node:${node.nodeType}`
61
70
  const attributes = []
@@ -69,8 +78,10 @@ export function durableProjection(node, ignoredAttribute, host = null, ignoredNo
69
78
  const children = []
70
79
  if (node.childNodes) {
71
80
  for (const child of node.childNodes) {
81
+ const ownChild = ownForm(child, form, host)
82
+ if (ownChild != null) { children.push(ownChild); continue }
72
83
  if (isIgnoredNode(child, ignoredNode)) continue
73
- children.push(durableProjection(child, ignoredAttribute, host, ignoredNode))
84
+ children.push(durableProjection(child, ignoredAttribute, host, ignoredNode, form, texts))
74
85
  }
75
86
  }
76
87
  const tag = String(node.tagName || '').toLowerCase()
@@ -153,7 +164,8 @@ function slotIndex(list, primitive) {
153
164
  export function compileCertificate(primitives, env = {}) {
154
165
  const isIgnored = env.isIgnored || null
155
166
  const ignoredAttribute = env.ignoreAttribute || null
156
- const normalized = normalizePrimitives(primitives)
167
+ const durableForm = env.durableForm || null
168
+ const normalized = normalizePrimitives(primitives, { sameContent })
157
169
 
158
170
  const structural = []
159
171
  for (const p of normalized) {
@@ -183,7 +195,7 @@ export function compileCertificate(primitives, env = {}) {
183
195
  if (move.at === item) steps.push(move.step)
184
196
  continue
185
197
  }
186
- steps.push(structuralStep(item, states.get(item), ignoredAttribute, isIgnored))
198
+ steps.push(structuralStep(item, states.get(item), ignoredAttribute, isIgnored, durableForm))
187
199
  }
188
200
  return { replayable: true, reason: null, steps }
189
201
  }
@@ -276,7 +288,7 @@ function pairMoves(structural, states) {
276
288
  return moves
277
289
  }
278
290
 
279
- function structuralStep(item, state, ignoredAttribute, ignoredNode) {
291
+ function structuralStep(item, state, ignoredAttribute, ignoredNode, durableForm = null) {
280
292
  const kind = item.primitive.kind
281
293
  return {
282
294
  type: kind === 'add' ? 'add' : 'restore',
@@ -285,7 +297,7 @@ function structuralStep(item, state, ignoredAttribute, ignoredNode) {
285
297
  seqBefore: state.before,
286
298
  seqAfter: state.after,
287
299
  slot: item.primitive,
288
- content: item.nodes.map((node) => durableProjection(node, ignoredAttribute, item.parent, ignoredNode)),
300
+ content: item.nodes.map((node) => durableProjection(node, ignoredAttribute, item.parent, ignoredNode, durableForm)),
289
301
  ids: kind === 'remove' ? item.nodes.flatMap((node) => durableIdsIn(node)) : [],
290
302
  targets: targetsOf(item.nodes, kind === 'add' ? 'added' : 'removed', item.parent),
291
303
  }
@@ -305,11 +317,13 @@ export function preflight(certificate, direction, env = {}) {
305
317
  const context = {
306
318
  isIgnored: env.isIgnored || null,
307
319
  ignoreAttribute: env.ignoreAttribute || null,
320
+ durableForm: env.durableForm || null,
308
321
  isProtected: env.isProtected || null,
309
322
  inScope: env.inScope || (() => true),
310
323
  liveNodesWithIds: env.liveNodesWithIds || (() => []),
311
324
  sim: new Map(),
312
325
  planned: new Map(),
326
+ texts: new Map(),
313
327
  operations: [],
314
328
  }
315
329
 
@@ -344,6 +358,12 @@ function refuseStep(code, targets) {
344
358
  return { code, targets: targets || [] }
345
359
  }
346
360
 
361
+ export function sameContent(a, b) {
362
+ if (a === b) return true
363
+ if (typeof a !== 'string' || typeof b !== 'string') return false
364
+ return a.replace(/\u00A0|&nbsp;/g, ' ') === b.replace(/\u00A0|&nbsp;/g, ' ')
365
+ }
366
+
347
367
  function liveValue(primitive) {
348
368
  if (primitive.kind === 'text') return primitive.target.data
349
369
  if (primitive.kind === 'value') {
@@ -369,11 +389,13 @@ function checkScalar(step, undoing, context) {
369
389
  if (primitive.kind === 'value' && typeof primitive.write === 'function' && typeof primitive.read !== 'function') {
370
390
  return refuseStep('unsupported', step.targets)
371
391
  }
372
- if (liveValue(primitive) !== expected) return refuseStep('content-changed', step.targets)
392
+ const live = liveValue(primitive)
393
+ if (primitive.content === true ? !sameContent(live, expected) : live !== expected) return refuseStep('content-changed', step.targets)
373
394
  if (primitive.kind === 'value') {
374
- context.operations.push({ type: 'value', target: primitive.target, prop: primitive.prop || 'value', value: next, read: primitive.read || null, write: primitive.write || null })
395
+ context.operations.push({ type: 'value', target: primitive.target, prop: primitive.prop || 'value', value: next, read: primitive.read || null, write: primitive.write || null, content: primitive.content === true })
375
396
  } else if (primitive.kind === 'text') {
376
397
  context.operations.push({ type: 'text', target: primitive.target, data: next })
398
+ context.texts.set(primitive.target, next)
377
399
  } else {
378
400
  context.operations.push({ type: 'attr', target: primitive.target, name: primitive.name, value: next == null ? null : next })
379
401
  }
@@ -387,7 +409,7 @@ function checkAdd(step, undoing, context) {
387
409
  const protectedTargets = protectedWithin(step.nodes, context.isProtected)
388
410
  if (protectedTargets.length > 0) return refuseStep('protected', protectedTargets)
389
411
  for (let i = 0; i < step.nodes.length; i++) {
390
- if (durableProjection(step.nodes[i], context.ignoreAttribute, step.parent, context.isIgnored) !== step.content[i]) return refuseStep('content-changed', step.targets)
412
+ if (durableProjection(step.nodes[i], context.ignoreAttribute, step.parent, context.isIgnored, context.durableForm) !== step.content[i]) return refuseStep('content-changed', step.targets)
391
413
  }
392
414
  for (const node of step.nodes) {
393
415
  context.operations.push({ type: 'remove', node })
@@ -422,7 +444,7 @@ function checkRestore(step, undoing, context) {
422
444
  if (!sameSequence(listFor(context, step.parent), step.seqAfter)) return refuseStep('structure-changed', step.targets)
423
445
  for (let i = 0; i < step.nodes.length; i++) {
424
446
  const node = step.nodes[i]
425
- if (node.parentNode || durableProjection(node, context.ignoreAttribute, step.parent, context.isIgnored) !== step.content[i]) return refuseStep('content-changed', step.targets)
447
+ if (node.parentNode || durableProjection(node, context.ignoreAttribute, step.parent, context.isIgnored, context.durableForm) !== step.content[i]) return refuseStep('content-changed', step.targets)
426
448
  }
427
449
  if (step.ids.length > 0 && liveDuplicates(step.ids, context)) return refuseStep('duplicate-id', step.targets)
428
450
  const at = slotIndex(step.seqAfter, step.slot)
@@ -445,9 +467,11 @@ function checkRestore(step, undoing, context) {
445
467
  // column before its column is removed) is leaving, so the removed subtree is
446
468
  // compared without it, as it was when it was recorded.
447
469
  const leaving = (node) => context.planned.get(node) === true || (typeof context.isIgnored === 'function' && context.isIgnored(node))
470
+ // Text an earlier step of this replay rewrites (a field emptied before its text node
471
+ // is taken out) is compared as that step leaves it, as it was when it was recorded.
448
472
  for (let i = 0; i < step.nodes.length; i++) {
449
473
  const node = step.nodes[i]
450
- if (node.parentNode !== step.parent || durableProjection(node, context.ignoreAttribute, step.parent, leaving) !== step.content[i]) return refuseStep('content-changed', step.targets)
474
+ if (node.parentNode !== step.parent || durableProjection(node, context.ignoreAttribute, step.parent, leaving, context.durableForm, context.texts) !== step.content[i]) return refuseStep('content-changed', step.targets)
451
475
  }
452
476
  for (const node of step.nodes) {
453
477
  context.operations.push({ type: 'remove', node })
@@ -505,6 +529,15 @@ function findChildByIdentity(parent, id) {
505
529
  return null
506
530
  }
507
531
 
532
+ function changedEnd(before, after) {
533
+ const max = Math.min(before.length, after.length)
534
+ let prefix = 0
535
+ while (prefix < max && before[prefix] === after[prefix]) prefix++
536
+ let suffix = 0
537
+ while (suffix < max - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++
538
+ return after.length - suffix
539
+ }
540
+
508
541
  function writeValue(op, value) {
509
542
  if (typeof op.write === 'function') op.write(op.target, value)
510
543
  else op.target[op.prop] = value
@@ -513,8 +546,9 @@ function writeValue(op, value) {
513
546
  export function applyOperations(operations) {
514
547
  const journal = []
515
548
  try {
516
- for (const op of operations) applyOne(op, journal)
517
- return { ok: true }
549
+ let caret = null
550
+ for (const op of operations) caret = applyOne(op, journal) || caret
551
+ return { ok: true, caret }
518
552
  } catch (error) {
519
553
  for (let i = journal.length - 1; i >= 0; i--) {
520
554
  try { journal[i]() } catch (_) { /* the rollback keeps trying the rest */ }
@@ -538,13 +572,16 @@ function applyOne(op, journal) {
538
572
  const previous = op.target.data
539
573
  op.target.data = op.data
540
574
  journal.push(() => { op.target.data = previous })
541
- return
575
+ return { node: op.target, offset: changedEnd(previous, op.data) }
542
576
  }
543
577
  if (op.type === 'value') {
544
578
  const previous = typeof op.read === 'function' ? op.read(op.target) : op.target[op.prop]
579
+ const textBefore = op.content ? op.target.textContent : null
545
580
  writeValue(op, op.value)
546
581
  journal.push(() => writeValue(op, previous))
547
- return
582
+ if (!op.content) return
583
+ const textAfter = op.target.textContent
584
+ return textAfter === textBefore ? undefined : { root: op.target, textOffset: changedEnd(textBefore, textAfter) }
548
585
  }
549
586
  if (op.type === 'insert') {
550
587
  const before = op.beforeId == null ? null : findChildByIdentity(op.parent, op.beforeId)
@@ -556,13 +593,17 @@ function applyOne(op, journal) {
556
593
  if (fromParent) fromParent.insertBefore(op.node, fromNext && fromNext.parentNode === fromParent ? fromNext : null)
557
594
  else if (op.node.parentNode === op.parent) op.parent.removeChild(op.node)
558
595
  })
559
- return
596
+ if (op.node.nodeType !== 3) return
597
+ return { node: op.node, offset: op.node.data.length }
560
598
  }
561
599
  if (op.type === 'remove') {
562
600
  const parent = op.node.parentNode
563
601
  if (!parent) throw new Error('hyper-undo: replay target is no longer attached')
564
602
  const next = op.node.nextSibling
603
+ const index = Array.prototype.indexOf.call(parent.childNodes, op.node)
565
604
  parent.removeChild(op.node)
566
605
  journal.push(() => parent.insertBefore(op.node, next))
606
+ if (op.node.nodeType !== 3) return
607
+ return { node: parent, offset: index }
567
608
  }
568
609
  }
package/src/scope.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { recordsToPrimitives } from './primitives.js'
2
- import { compileCertificate, preflight, applyOperations, described } from './replay-plan.js'
2
+ import { compileCertificate, preflight, applyOperations, described, sameContent } from './replay-plan.js'
3
3
  import { shouldIgnore } from './filter.js'
4
4
  import { createEmitter } from './emitter.js'
5
+ import { watchTyping } from './typing.js'
5
6
 
6
7
  const DEFAULTS = {
7
8
  maxHistory: 100,
@@ -11,6 +12,9 @@ const DEFAULTS = {
11
12
  ignoreAttribute: null,
12
13
  isProtected: null,
13
14
  ignoreNode: null,
15
+ durableForm: null,
16
+ typingBreak: null,
17
+ refused: null,
14
18
  debug: false,
15
19
  }
16
20
 
@@ -32,6 +36,10 @@ export function createScope(opts) {
32
36
  let recordingMode = config.recording === 'commands' ? 'commands' : 'automatic'
33
37
  let capture = null // { records, primitives } while an explicit command runs
34
38
  let replaying = false // true only inside an undo/redo preflight and apply
39
+ let tracked = new WeakMap() // root element -> { prop, read, write, baseline }
40
+ let composing = false // an input method composition is open
41
+ let movedUnderStep = false // a paused writer changed what the open step changed
42
+ let typingCleanup = null // watchTyping's cleanup, kept for stop()
35
43
 
36
44
  function log(...args) { if (config.debug) console.log('[hyper-undo]', ...args) }
37
45
 
@@ -49,6 +57,15 @@ export function createScope(opts) {
49
57
  attributeOldValue: true,
50
58
  characterDataOldValue: true,
51
59
  })
60
+ typingCleanup = watchTyping(config.scope, {
61
+ sentence: config.typingBreak === 'sentence',
62
+ stepOpen: () => {
63
+ if (pauseDepth === 0 && observer) handleRecords(observer.takeRecords())
64
+ return pendingPrimitives.length > 0
65
+ },
66
+ endStep: flush,
67
+ composing: setComposing,
68
+ })
52
69
  log('started')
53
70
  }
54
71
 
@@ -74,6 +91,8 @@ export function createScope(opts) {
74
91
  if (!started) return
75
92
  started = false
76
93
  if (observer) { observer.disconnect(); observer = null }
94
+ if (typingCleanup) { typingCleanup(); typingCleanup = null }
95
+ composing = false
77
96
  clearIdleTimer()
78
97
  undoStack.length = 0
79
98
  redoStack.length = 0
@@ -82,6 +101,7 @@ export function createScope(opts) {
82
101
  capture = null
83
102
  replaying = false
84
103
  pauseDepth = 0 // reset so a reused (created) scope doesn't restart paused
104
+ tracked = new WeakMap()
85
105
  log('stopped')
86
106
  }
87
107
 
@@ -93,6 +113,70 @@ export function createScope(opts) {
93
113
  return !!(el && el.nodeType === 1 && el.isConnected === false)
94
114
  }
95
115
 
116
+ // --- Tracked content ---
117
+
118
+ function readContent(root, read) {
119
+ try { const value = read(root); return typeof value === 'string' ? value : null } catch (_) { return null }
120
+ }
121
+
122
+ function trackContent(root, { read, write, prop = 'content' } = {}) {
123
+ if (!root || typeof read !== 'function' || typeof write !== 'function') return () => {}
124
+ const state = { prop, read, write, baseline: readContent(root, read) }
125
+ tracked.set(root, state)
126
+ return () => { if (tracked.get(root) === state) tracked.delete(root) }
127
+ }
128
+
129
+ function trackedRootOf(node) {
130
+ for (let n = node; n; n = n.parentNode) if (tracked.has(n)) return n
131
+ return null
132
+ }
133
+
134
+ function isTrackedContent(record) {
135
+ const root = trackedRootOf(record.target)
136
+ return !!root && !(record.type === 'attributes' && record.target === root)
137
+ }
138
+
139
+ function contentRootsIn(records) {
140
+ const roots = new Set()
141
+ for (const record of records) if (isTrackedContent(record)) roots.add(trackedRootOf(record.target))
142
+ return roots
143
+ }
144
+
145
+ function sampleContent(roots) {
146
+ const out = []
147
+ for (const root of roots) {
148
+ const state = tracked.get(root)
149
+ const now = readContent(root, state.read)
150
+ if (now == null) continue
151
+ if (sameContent(now, state.baseline)) { state.baseline = now; continue }
152
+ if (state.baseline != null) out.push({ kind: 'value', target: root, prop: state.prop, oldValue: state.baseline, newValue: now, read: state.read, write: state.write, content: true })
153
+ state.baseline = now
154
+ }
155
+ return out
156
+ }
157
+
158
+ function rebaseContent(roots) {
159
+ for (const root of roots) {
160
+ const state = tracked.get(root)
161
+ state.baseline = readContent(root, state.read)
162
+ }
163
+ }
164
+
165
+ // Records another writer made while this scope was paused are dropped. When one of
166
+ // them changed a text node, an attribute or a tracked field the open step has already
167
+ // changed, the step has to close before this tab types again: joined with that typing
168
+ // it would run from before the other writer's change to after it, and its undo would
169
+ // take their change back.
170
+ function dropPaused(records) {
171
+ const roots = contentRootsIn(records)
172
+ if (!movedUnderStep && pendingPrimitives.some((p) => p.kind === 'value'
173
+ ? roots.has(p.target)
174
+ : records.some((r) => r.target === p.target && (r.type === 'characterData' ? p.kind === 'text' : r.type === 'attributes' && r.attributeName === p.name)))) {
175
+ movedUnderStep = true
176
+ }
177
+ rebaseContent(roots)
178
+ }
179
+
96
180
  // --- Observer callback ---
97
181
 
98
182
  // The record source's callback. It hands over its own instance as a second
@@ -115,9 +199,10 @@ export function createScope(opts) {
115
199
  // Commands mode: an unbracketed record is some other writer's (a merge, a
116
200
  // sap paint, a sortable gesture). It changes the document, so later guards
117
201
  // see it, but it is not a local action and earns no entry.
118
- if (recordingMode === 'commands') return
119
- if (pauseDepth > 0) return
202
+ if (recordingMode === 'commands') { rebaseContent(contentRootsIn(records)); return }
203
+ if (pauseDepth > 0) { dropPaused(records); return }
120
204
  const prims = convertRecords(dropIgnoredRecords(records), bracketed)
205
+ for (const p of sampleContent(contentRootsIn(records))) prims.push(p)
121
206
  if (prims.length > 0) {
122
207
  for (const p of prims) pendingPrimitives.push(p)
123
208
  restartIdleTimer()
@@ -155,15 +240,27 @@ export function createScope(opts) {
155
240
  // fields too; head's <title> stays with the bracket's own title capture.
156
241
  function convertRecords(records, bracketed = false) {
157
242
  const removedHere = removedFromScope(records)
158
- const kept = records.filter((r) => (observedHere(r.target) || removedHere(r.target) || (bracketed && inHead(r.target))) && !shouldIgnore(r.target, config.ignoreAttribute, r))
243
+ const kept = records.filter((r) => (observedHere(r.target) || removedHere(r.target) || (bracketed && inHead(r.target))) && !isTrackedContent(r) && !shouldIgnore(r.target, config.ignoreAttribute, r))
159
244
  return recordsToPrimitives(kept, (node) => shouldIgnore(node, config.ignoreAttribute))
160
245
  }
161
246
 
162
247
  // --- Idle batching ---
163
248
 
249
+ function setComposing(on) {
250
+ composing = !!on
251
+ if (!composing && pendingPrimitives.length > 0) restartIdleTimer()
252
+ }
253
+
164
254
  function restartIdleTimer() {
165
255
  clearIdleTimer()
166
- idleTimer = setTimeout(closeIdleBatch, config.idleWindowMs)
256
+ idleTimer = setTimeout(onIdle, config.idleWindowMs)
257
+ }
258
+ // A step never closes while a composition is open: the timer waits it out and
259
+ // the composed text lands in one entry.
260
+ function onIdle() {
261
+ idleTimer = null
262
+ if (composing) { idleTimer = setTimeout(onIdle, config.idleWindowMs); return }
263
+ closeIdleBatch()
167
264
  }
168
265
  function clearIdleTimer() {
169
266
  if (idleTimer != null) { clearTimeout(idleTimer); idleTimer = null }
@@ -179,7 +276,7 @@ export function createScope(opts) {
179
276
  // --- Commit machinery ---
180
277
 
181
278
  function pushCommit(label, primitives) {
182
- const certificate = compileCertificate(primitives, { isIgnored: isIgnoredNode, ignoreAttribute: isIgnoredAttribute })
279
+ const certificate = compileCertificate(primitives, { isIgnored: isIgnoredNode, ignoreAttribute: isIgnoredAttribute, durableForm: config.durableForm })
183
280
  if (certificate.steps.length === 0) {
184
281
  log('commit', label, 'with nothing to record')
185
282
  emitter.emit('commit')
@@ -254,20 +351,22 @@ export function createScope(opts) {
254
351
  // If an OUTER pause is still active (this capture frame is nested inside an
255
352
  // exclusion pause), honor the outer "exclude everything" contract: discard
256
353
  // the captured records instead of committing through the outer pause.
257
- if (pauseDepth > 1) { observer.takeRecords(); return }
354
+ if (pauseDepth > 1) { rebaseContent(contentRootsIn(observer.takeRecords())); return }
258
355
  // Close any pending idle batch (e.g. in-progress typing captured BEFORE the
259
356
  // pause) as its own commit FIRST, so the structural commit we're about to
260
357
  // push lands AFTER it in history. Without this, type-then-immediately-add
261
358
  // would record the structural op before the typing, inverting undo order.
262
359
  flush()
263
- const primitives = convertRecords(observer.takeRecords(), true)
360
+ const records = observer.takeRecords()
361
+ const primitives = convertRecords(records, true)
362
+ for (const p of sampleContent(contentRootsIn(records))) primitives.push(p)
264
363
  if (primitives.length === 0) return
265
364
  clearIdleTimer()
266
365
  pushCommit(label, primitives)
267
366
  }
268
367
 
269
368
  function discardCaptured() {
270
- if (observer) observer.takeRecords()
369
+ if (observer) rebaseContent(contentRootsIn(observer.takeRecords()))
271
370
  }
272
371
 
273
372
  // --- Explicit commands ---
@@ -333,6 +432,7 @@ export function createScope(opts) {
333
432
  capture = null
334
433
  if (!held) return []
335
434
  const primitives = convertRecords(held.records, true)
435
+ for (const p of sampleContent(contentRootsIn(held.records))) primitives.push(p)
336
436
  for (const p of held.primitives) primitives.push(p)
337
437
  return primitives
338
438
  }
@@ -465,7 +565,14 @@ export function createScope(opts) {
465
565
  if (pauseDepth === 0) return // underflow guard
466
566
  pauseDepth--
467
567
  // Discard records captured during the pause only on the outermost release.
468
- if (pauseDepth === 0 && observer) observer.takeRecords()
568
+ if (pauseDepth === 0 && observer) {
569
+ dropPaused(observer.takeRecords())
570
+ if (movedUnderStep) {
571
+ movedUnderStep = false
572
+ clearIdleTimer()
573
+ closeIdleBatch()
574
+ }
575
+ }
469
576
  log('resumed', pauseDepth)
470
577
  }
471
578
 
@@ -474,51 +581,88 @@ export function createScope(opts) {
474
581
  // One entry is applied whole or not at all. Preflight compares the certificate
475
582
  // against the live document without writing; only a fully validated plan
476
583
  // reaches the DOM, and a native failure rolls it back and keeps both stacks.
584
+ // With `refused: 'skip'` a refused entry is dropped and the call goes on to the
585
+ // next one, so a step that can no longer be replayed never blocks older ones.
477
586
  function replay(direction, requestedId) {
478
587
  if (scopeDisconnected()) { stop(); return { ok: false, code: 'disconnected' } }
588
+ // A consumer can hold replay (an open gesture, a pending save) without
589
+ // refusing the entry: the stacks and the document stay as they are.
590
+ if (typeof config.holdReplay === 'function' && config.holdReplay()) return { ok: false, code: 'held' }
479
591
  flush()
480
592
  const source = direction === 'undo' ? undoStack : redoStack
481
593
  const destination = direction === 'undo' ? redoStack : undoStack
482
- const entry = source.at(-1)
594
+ const skipped = []
595
+ let entry = source.at(-1)
483
596
  if (!entry) return { ok: false, code: 'empty' }
484
597
  if (requestedId && requestedId !== entry.id) {
485
598
  const requested = findEntry(requestedId) || entry
486
599
  return refuse(requested, direction, 'stale-entry')
487
600
  }
488
- let checked
489
- replaying = true
490
- try {
491
- checked = preflight(entry.certificate, direction, {
492
- isIgnored: isIgnoredNode,
493
- ignoreAttribute: isIgnoredAttribute,
494
- isProtected: isProtectedNode,
495
- inScope,
496
- liveNodesWithIds,
497
- })
498
- } finally {
499
- replaying = false
500
- }
501
- if (!checked.ok) return refuse(entry, direction, checked.code, checked.targets)
502
- pause()
503
- let applied
504
- replaying = true
505
- try {
506
- applied = applyOperations(checked.operations)
507
- } finally {
508
- replaying = false
509
- resume()
510
- }
511
- if (!applied.ok) {
512
- log(`${direction} failed and was rolled back`, applied.error)
513
- return refuse(entry, direction, 'replay-error')
601
+ const canSkip = config.refused === 'skip' && !requestedId
602
+ while (entry) {
603
+ let checked
604
+ replaying = true
605
+ try {
606
+ checked = preflight(entry.certificate, direction, {
607
+ isIgnored: isIgnoredNode,
608
+ ignoreAttribute: isIgnoredAttribute,
609
+ durableForm: config.durableForm,
610
+ isProtected: isProtectedNode,
611
+ inScope,
612
+ liveNodesWithIds,
613
+ })
614
+ } finally {
615
+ replaying = false
616
+ }
617
+ if (!checked.ok) {
618
+ if (!canSkip) return refuse(entry, direction, checked.code, checked.targets)
619
+ skipEntry(entry, direction, checked.code, checked.targets, skipped)
620
+ entry = source.at(-1)
621
+ continue
622
+ }
623
+ pause()
624
+ let applied
625
+ replaying = true
626
+ try {
627
+ applied = applyOperations(checked.operations)
628
+ } finally {
629
+ replaying = false
630
+ resume()
631
+ }
632
+ if (!applied.ok) {
633
+ log(`${direction} failed and was rolled back`, applied.error)
634
+ if (!canSkip) return refuse(entry, direction, 'replay-error')
635
+ skipEntry(entry, direction, 'replay-error', [], skipped)
636
+ entry = source.at(-1)
637
+ continue
638
+ }
639
+ rebaseContent(new Set(checked.operations.filter((op) => op.type === 'value' && tracked.has(op.target)).map((op) => op.target)))
640
+ source.pop()
641
+ destination.push(entry)
642
+ refusedTop = null
643
+ const receipt = { ok: true, entryId: entry.id, label: entry.label, direction }
644
+ if (skipped.length) receipt.skipped = skipped
645
+ if (applied.caret) receipt.caret = applied.caret
646
+ log(direction, entry.label)
647
+ emitter.emit(direction, receipt)
648
+ return receipt
514
649
  }
515
- source.pop()
516
- destination.push(entry)
650
+ return { ok: false, code: 'empty', direction, skipped }
651
+ }
652
+
653
+ // Drop a refused entry in the same press. Its description is built while it is
654
+ // still on its stack (describe() reads refusedTop), so a consumer can keep a
655
+ // recovery copy of what the removed entry would have restored.
656
+ function skipEntry(entry, direction, code, targets, skipped) {
657
+ refusedTop = { entryId: entry.id, direction, code, targets }
658
+ const description = describe(entry.id)
659
+ const stack = direction === 'undo' ? undoStack : redoStack
660
+ stack.pop()
517
661
  refusedTop = null
518
- const receipt = { ok: true, entryId: entry.id, label: entry.label, direction }
519
- log(direction, entry.label)
520
- emitter.emit(direction, receipt)
521
- return receipt
662
+ const record = { entryId: entry.id, label: entry.label, direction, code, targets, description }
663
+ log('skipped', direction, code)
664
+ emitter.emit('skipped', record)
665
+ skipped.push(record)
522
666
  }
523
667
 
524
668
  function undo(opts) { return replay('undo', opts && opts.entryId) }
@@ -573,6 +717,15 @@ export function createScope(opts) {
573
717
  }
574
718
  }
575
719
 
720
+ // A host's durable form is stored as #form:"<json string>"; a description shows the
721
+ // string itself, not the token.
722
+ function readableForm(content) {
723
+ if (typeof content !== 'string') return null
724
+ return content.replace(/#form:("(?:[^"\\]|\\.)*")/g, (_, json) => {
725
+ try { return JSON.parse(json) } catch (_) { return '' }
726
+ })
727
+ }
728
+
576
729
  function describeStep(step) {
577
730
  if (step.type === 'move') {
578
731
  return {
@@ -588,7 +741,7 @@ export function createScope(opts) {
588
741
  return {
589
742
  kind: step.type === 'add' ? 'add' : 'remove',
590
743
  parent: { id: described(step.parent).id },
591
- nodes: step.nodes.map((node, i) => ({ id: described(node).id, html: step.content[i] ?? null })),
744
+ nodes: step.nodes.map((node, i) => ({ id: described(node).id, html: readableForm(step.content[i]) })),
592
745
  }
593
746
  }
594
747
  const primitive = step.primitive
@@ -616,7 +769,9 @@ export function createScope(opts) {
616
769
  grew = false
617
770
  for (const r of records) {
618
771
  if (r.type !== 'childList' || !r.removedNodes?.length) continue
772
+ if (isTrackedContent(r)) continue
619
773
  if (!observedHere(r.target) && !insideRemoved(r.target, removed)) continue
774
+ if (shouldIgnore(r.target, config.ignoreAttribute, r)) continue
620
775
  for (const node of r.removedNodes) if (!removed.has(node)) { removed.add(node); grew = true }
621
776
  }
622
777
  }
@@ -630,7 +785,7 @@ export function createScope(opts) {
630
785
 
631
786
  function observedHere(node) {
632
787
  const root = config.scope
633
- if (!root || !node) return false
788
+ if (!root || !node || typeof node.nodeType !== 'number') return false
634
789
  return node === root || (typeof root.contains === 'function' && root.contains(node))
635
790
  }
636
791
 
@@ -677,7 +832,7 @@ export function createScope(opts) {
677
832
  // Discard any same-tick buffered records too, so a bootstrap mutation
678
833
  // followed by clear() in the same turn doesn't resurface as a commit once
679
834
  // the observer callback fires.
680
- if (observer) observer.takeRecords()
835
+ if (observer) rebaseContent(contentRootsIn(observer.takeRecords()))
681
836
  undoStack.length = 0
682
837
  redoStack.length = 0
683
838
  pendingPrimitives = []
@@ -691,10 +846,12 @@ export function createScope(opts) {
691
846
 
692
847
  return {
693
848
  _config: config, // private: used by keys.js to read shadowKeydownIn
849
+ _tracksContentAt: (node) => recordingMode !== 'commands' && !!trackedRootOf(node), // private: keys.js rule 4
850
+ _recordsAt: (node) => recordingMode !== 'commands' && !isIgnoredNode(node), // private: keys.js rule 4
694
851
  start, stop,
695
852
  commit, commitCaptured, discardCaptured, flush,
696
853
  command, setRecording,
697
- record, recordValue,
854
+ record, recordValue, trackContent,
698
855
  undo, redo, clear, discard, describe,
699
856
  pause, resume,
700
857
  on: emitter.on, off: emitter.off,
package/src/typing.js ADDED
@@ -0,0 +1,142 @@
1
+ // Every listener sits on the scope root, in the capture phase: typing only ever happens
2
+ // inside it, and the window keeps only the history key bindings.
3
+ // A sentence end, a field switch, a change of intent, a command, a replaced
4
+ // selection and a caret jump each close the open typing step, so one undo press
5
+ // undoes one sentence of typing. The idle timer never closes a step while an
6
+ // input method composition is open.
7
+
8
+ const SENTENCE_END = /[.?!]["'\u201D\u2019)\]]*$/
9
+ const LINE_BREAKS = new Set(['insertParagraph', 'insertLineBreak'])
10
+ const ALONE = new Set([
11
+ 'insertFromPaste',
12
+ 'insertFromDrop',
13
+ 'insertFromYank',
14
+ 'insertReplacementText',
15
+ 'deleteByCut',
16
+ 'deleteByDrag',
17
+ ])
18
+ const TYPING_KEYS = new Set(['Enter', 'Backspace', 'Delete'])
19
+ const NAV_KEYS = new Set([
20
+ 'ArrowLeft',
21
+ 'ArrowRight',
22
+ 'ArrowUp',
23
+ 'ArrowDown',
24
+ 'Home',
25
+ 'End',
26
+ 'PageUp',
27
+ 'PageDown',
28
+ ])
29
+
30
+ export function endsSentence(textBefore) {
31
+ return SENTENCE_END.test(textBefore)
32
+ }
33
+
34
+ export function watchTyping(root, { sentence, endStep, stepOpen = () => true, composing }) {
35
+ const view = root.ownerDocument && root.ownerDocument.defaultView
36
+ if (!view) return () => {}
37
+ const inside = (node) => !!node && typeof node.nodeType === 'number' && (node === root || root.contains(node))
38
+ let breakAfter = false
39
+ let jumped = false
40
+ let early = false
41
+ let last = { kind: 'other', host: null }
42
+
43
+ const kindOf = (type) => ALONE.has(type) || type.startsWith('format') ? 'alone'
44
+ : type.startsWith('delete') ? 'delete'
45
+ : type.startsWith('insert') ? 'insert' : 'other'
46
+
47
+ const hostOf = (target) => {
48
+ if (!target) return null
49
+ const el = target.nodeType === 1 ? target : target.parentElement
50
+ const host = el && el.closest && el.closest('[contenteditable]:not([contenteditable="false"])')
51
+ return host || target
52
+ }
53
+
54
+ // A rich editor may cancel the line break and insert the paragraph itself, so
55
+ // no input event follows: the timer closes the step either way, and the flush
56
+ // takes the pending records synchronously.
57
+ const closeAfter = () => {
58
+ if (!breakAfter) return
59
+ breakAfter = false
60
+ endStep()
61
+ }
62
+
63
+ const onBeforeInput = (event) => {
64
+ if (!sentence || event.isComposing || !inside(event.target)) return
65
+ const type = event.inputType || ''
66
+ const kind = kindOf(type)
67
+ const host = hostOf(event.target)
68
+ const selection = view.getSelection && view.getSelection()
69
+ const collapsed = !selection || selection.isCollapsed
70
+ const wasJumped = jumped
71
+ jumped = false
72
+ const wasEarly = early
73
+ early = false
74
+ if (
75
+ kind === 'alone' ||
76
+ (!wasEarly && kind !== last.kind && kind !== 'other' && last.kind !== 'other') ||
77
+ (!wasEarly && ((last.host !== null && host !== last.host) || !collapsed || wasJumped))
78
+ ) {
79
+ endStep()
80
+ }
81
+ last = { kind, host }
82
+ if (kind === 'alone') {
83
+ breakAfter = true
84
+ setTimeout(closeAfter, 0)
85
+ return
86
+ }
87
+ const space = type === 'insertText' && (event.data === ' ' || event.data === '\u00A0')
88
+ if (!space && !LINE_BREAKS.has(type)) return
89
+ if (space && !stepOpen()) return
90
+ if (!endsSentence(textBeforeCaret(view))) return
91
+ breakAfter = true
92
+ setTimeout(closeAfter, 0)
93
+ }
94
+ const onInput = (event) => { if (inside(event.target)) closeAfter() }
95
+ const onStart = (event) => { if (inside(event.target)) composing(true) }
96
+ const onEnd = (event) => { if (inside(event.target)) composing(false) }
97
+ const onPointerDown = (event) => { if (inside(event.target)) jumped = true }
98
+ // A rich editor may write in its own keydown, before beforeinput: it deletes the
99
+ // selection the key replaces, and it tidies its markup on the first key. A caret jump,
100
+ // a field switch and a replaced selection close the step here, so those writes belong
101
+ // to the step the key starts.
102
+ const onKeyDown = (event) => {
103
+ if (!inside(event.target)) return
104
+ if (NAV_KEYS.has(event.key)) { jumped = true; return }
105
+ if (!sentence || event.isComposing || event.keyCode === 229 || event.ctrlKey || event.metaKey) return
106
+ if (!event.key || (event.key.length !== 1 && !TYPING_KEYS.has(event.key))) return
107
+ const selection = view.getSelection && view.getSelection()
108
+ const switched = last.host !== null && hostOf(event.target) !== last.host
109
+ if (!jumped && !switched && (!selection || selection.isCollapsed)) return
110
+ endStep()
111
+ jumped = false
112
+ early = true
113
+ setTimeout(() => { early = false }, 0)
114
+ }
115
+ root.addEventListener('beforeinput', onBeforeInput, true)
116
+ root.addEventListener('input', onInput, true)
117
+ root.addEventListener('compositionstart', onStart, true)
118
+ root.addEventListener('compositionend', onEnd, true)
119
+ root.addEventListener('pointerdown', onPointerDown, true)
120
+ root.addEventListener('keydown', onKeyDown, true)
121
+ return () => {
122
+ root.removeEventListener('beforeinput', onBeforeInput, true)
123
+ root.removeEventListener('input', onInput, true)
124
+ root.removeEventListener('compositionstart', onStart, true)
125
+ root.removeEventListener('compositionend', onEnd, true)
126
+ root.removeEventListener('pointerdown', onPointerDown, true)
127
+ root.removeEventListener('keydown', onKeyDown, true)
128
+ }
129
+ }
130
+
131
+ function textBeforeCaret(view) {
132
+ const selection = view.getSelection && view.getSelection()
133
+ if (!selection || !selection.isCollapsed || !selection.anchorNode) return ''
134
+ const anchor = selection.anchorNode
135
+ const element = anchor.nodeType === 1 ? anchor : anchor.parentElement
136
+ const host = element && element.closest('[contenteditable]:not([contenteditable="false"])')
137
+ if (!host) return ''
138
+ const range = host.ownerDocument.createRange()
139
+ range.setStart(host, 0)
140
+ range.setEnd(anchor, selection.anchorOffset)
141
+ return range.toString().replace(/\u200B/g, '').slice(-8)
142
+ }