Add phase 2 inline extensions & clipboard#86
Conversation
Covers superscript/subscript, hyperlinks, clipboard operations, and find & replace for the Docs word processor. Co-Authored-By: Claude Opus 4.6 <[email protected]>
14 tasks covering superscript/subscript, hyperlinks, clipboard, and find & replace with TDD approach. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add superscript and subscript boolean fields to InlineStyle interface, update inlineStylesEqual() to compare them, and enforce mutual exclusion in applyStyleToBlock() so applying one clears the other. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Superscript and subscript runs now use 60% of the original font size for text measurement and Canvas rendering. Superscript shifts the baseline up by 40% of the original font size; subscript shifts it down by 20%. Line height still uses the original (unreduced) font size so lines with only super/subscript text are not clipped. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add Cmd+. / Cmd+, keyboard shortcuts for superscript/subscript toggle, include both properties in clearFormatting, and add toolbar buttons with tabler icons. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add href string property to InlineStyle, update inlineStylesEqual() to compare it, and include it in clearFormatting(). Tests cover equality detection, applying href to text, and removing it. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add EditorAPI methods (insertLink, removeLink, getLinkAtCursor, onLinkRequest) for hyperlink management. Wire Cmd/Ctrl+K shortcut in TextEditor to trigger a link request callback. Add link toolbar button with window.prompt for URL entry. Serialize/deserialize href in Yorkie doc store. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add hover detection for hyperlinks in the canvas editor that shows a DOM popover with the URL, open/edit/remove buttons. Ctrl+Click (or Cmd+Click on Mac) opens the link directly in a new tab. Co-Authored-By: Claude Opus 4.6 <[email protected]>
When the user types a space or presses Enter after a URL (http:// or https://), the preceding token is automatically detected and converted into a clickable hyperlink by applying an href inline style. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add clipboard serialization module with custom MIME type for preserving formatting during copy/paste within the docs editor. Copy/cut now serializes selected blocks with full inline styles and block properties. Paste checks for internal format first, falling back to plain text. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add parseHtmlToInlines() to convert pasted HTML into formatted Inline objects, mapping common tags (b/strong, i/em, u, s/del/strike, a) and CSS properties (color, font-size, background-color) to InlineStyle. Wire HTML paste into handlePaste with priority: internal JSON > HTML > plain text. Add Cmd/Ctrl+Shift+V shortcut for explicit plain-text paste (strips formatting). Extract insertPlainText() helper to share logic between paste handler and shortcut. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add format painter shortcuts: Cmd+Shift+C captures inline styles at the cursor into a style buffer; Cmd+Alt+V applies the buffered styles to the current selection. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add SearchOptions and SearchMatch types, implement Doc.searchText() with case-insensitive default, case-sensitive option, and regex support. Searches across all blocks using concatenated inline text. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add search highlight state (searchMatches, activeMatchIndex) to the editor and pass computed highlight rectangles to the canvas renderer. Inactive matches render in yellow (#fff2a8), the active match in orange (#f4a939). Highlights draw behind peer and local selections. Co-Authored-By: Claude Opus 4.6 <[email protected]>
FindReplaceState with next/previous/replace/replaceAll, Cmd+F/H shortcuts, find bar React component with case-sensitive and regex toggles. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Change link popover trigger from hover to cursor position - Use position:fixed + Portal for reliable popover positioning - Add inline URL input popover replacing window.prompt() - Unify view/edit modes into single DocsLinkPopover component - Fix horizontal scroll offset in mouse coordinate mapping - Fix superscript/subscript cursor and selection misalignment - Remove redundant Open button (URL text is clickable) - Reorganize toolbar: undo,redo|style|B,I,U,color,bg|link|align,lists,indent Co-Authored-By: Claude Opus 4.6 <[email protected]>
📝 WalkthroughWalkthroughAdds Phase 2 features: superscript/subscript and hyperlink inline styles; clipboard serialization and HTML→blocks parsing; URL auto-detection and link UI/commands; document-level search & replace; editor API and UI integrations; rendering/layout and Yorkie serialization updates; plus tests and roadmap/spec docs. Changes
Sequence Diagram(s)sequenceDiagram
rect rgba(220,220,255,0.5)
actor User
participant EditorAPI
participant FindReplaceState
participant Doc
participant DocCanvas
end
User->>EditorAPI: onFindRequest()
EditorAPI->>FindReplaceState: create/get(doc)
User->>FindReplaceState: search(query, options)
FindReplaceState->>Doc: searchText(query, options)
Doc-->>FindReplaceState: SearchMatch[]
FindReplaceState->>EditorAPI: setSearchMatches(matches, activeIndex)
EditorAPI->>DocCanvas: render(..., searchHighlightRects, activeSearchIndex)
DocCanvas-->>User: highlighted matches rendered
sequenceDiagram
rect rgba(220,255,220,0.5)
actor User
participant TextEditor
participant EditorAPI
participant Doc
participant DocsLinkPopover
participant DocCanvas
end
User->>TextEditor: type URL or press (mod+K)
TextEditor->>EditorAPI: requestLink()/insertLink(url)
EditorAPI->>Doc: applyInlineStyle({href: url}, selection)
Doc-->>EditorAPI: updated inlines
EditorAPI->>DocCanvas: render()
DocCanvas-->>User: link rendered
User->>TextEditor: move cursor into link
TextEditor->>EditorAPI: onCursorLinkChange({href, rect})
EditorAPI->>DocsLinkPopover: show(href, rect)
sequenceDiagram
rect rgba(255,220,220,0.5)
actor User
participant TextEditor
participant Clipboard
participant Doc
participant DocCanvas
end
User->>TextEditor: Ctrl/Cmd+C
TextEditor->>Clipboard: serializeBlocks(blocks) (WAFFLEDOCS_MIME + text/plain)
User->>TextEditor: Ctrl/Cmd+V
TextEditor->>Clipboard: read WAFFLEDOCS_MIME?
alt WAFFLEDOCS available
Clipboard-->>TextEditor: blocks
TextEditor->>Doc: insertBlocks(blocks)
else HTML available
Clipboard-->>TextEditor: html
TextEditor->>Clipboard: parseHtmlToBlocks(html)
TextEditor->>Doc: insertBlocks(parsed)
else plain text
TextEditor->>Doc: insertPlainText(text)
end
Doc-->>DocCanvas: render updated content
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Resolve conflicts in editor.ts (keep link methods + optional chaining) and text-editor.ts (keep private field refactor + callback properties). Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add null checks for textEditor (nullable after main refactor) - Track shift key state for ClipboardEvent paste handler - Fix mock ctx type assertions in layout tests Co-Authored-By: Claude Opus 4.6 <[email protected]>
Verification: verify:selfResult: ✅ PASS in 105.6s
Verification: verify:integrationResult: ✅ PASS |
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (3)
packages/docs/test/model/types.test.ts (1)
135-143: Consider adding a test for differing href values.The current test only checks
hrefpresent vs absent. Adding a case for two different href strings would strengthen coverage.💡 Suggested additional test case
it('should detect href difference', () => { expect(inlineStylesEqual({ href: 'https://example.com' }, {})).toBe(false); expect( inlineStylesEqual( { href: 'https://example.com' }, { href: 'https://example.com' }, ), ).toBe(true); + expect( + inlineStylesEqual( + { href: 'https://example.com' }, + { href: 'https://other.com' }, + ), + ).toBe(false); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/test/model/types.test.ts` around lines 135 - 143, Add a test that asserts inlineStylesEqual returns false for two different href values: inside the existing "should detect href difference" test (or as a new it block) call inlineStylesEqual({ href: 'https://example.com' }, { href: 'https://other.com' }) and expect(false); this uses the inlineStylesEqual function to verify differing href strings are considered unequal.packages/docs/src/view/layout.ts (1)
256-259: Extract the 0.6 superscript/subscript scale factor to a shared constant.The
0.6multiplier for superscript/subscript font scaling is duplicated across multiple files (layout.ts,selection.ts,peer-cursor.ts,doc-canvas.ts). Defining a constant likeSUPERSCRIPT_SCALE = 0.6intheme.tswould centralize this value and reduce maintenance burden if it needs adjustment.♻️ Suggested constant extraction
In
packages/docs/src/view/theme.ts:export const SUPERSCRIPT_SCALE = 0.6;Then in this file:
+import { Theme, buildFont, ptToPx, SUPERSCRIPT_SCALE } from './theme.js'; // ... const charFontSize = isSuperOrSub - ? (seg.style.fontSize ?? Theme.defaultFontSize) * 0.6 + ? (seg.style.fontSize ?? Theme.defaultFontSize) * SUPERSCRIPT_SCALE : seg.style.fontSize;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/layout.ts` around lines 256 - 259, Extract the magic 0.6 multiplier into a single exported constant (e.g., SUPERSCRIPT_SCALE = 0.6) in your theme module and import it here; replace the inline 0.6 used when computing charFontSize (the ternary using seg.style.superscript || seg.style.subscript and Theme.defaultFontSize) with SUPERSCRIPT_SCALE so all files (layout.ts, selection.ts, peer-cursor.ts, doc-canvas.ts) can reuse the same value.packages/docs/src/model/document.ts (1)
331-345: Add JSDoc comments documenting the undo-boundary requirement for these low-level helpers.
updateBlockDirect()andinsertBlockAt()mutate the store without snapshotting internally. While they're currently safe (private methods called only fromhandlePaste(), which always establishes the boundary first), their undo-boundary contract should be explicit in the method signatures to prevent misuse if they're later promoted to public APIs or called from new contexts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/model/document.ts` around lines 331 - 345, The methods updateBlockDirect and insertBlockAt perform direct mutations on this.store without creating an undo snapshot, so callers must establish an undo boundary before invoking them; add JSDoc above each method (updateBlockDirect(blockId: string, block: Block): void and insertBlockAt(index: number, block: Block): void) explicitly stating that they do not snapshot state, mutate the store in-place, and require the caller to create an undo boundary (e.g., via the surrounding handlePaste logic or a start/commit undo API) before calling to avoid breaking undo/redo behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/tasks/active/20260326-docs-phase2-todo.md`:
- Around line 1-11: The docs task is missing its paired lessons file; create
docs/tasks/active/20260326-docs-phase2-lessons.md alongside the existing
20260326-docs-phase2-todo.md, include a matching title/metadata referencing the
Todo entry, a short summary of implementation learnings, a lessons checklist
template (what went well, blockers, follow-ups), and links back to the spec
docs/superpowers/specs/2026-03-26-docs-phase2-inline-clipboard-design.md so the
task and lessons remain linked for tracking and future retrospectives.
In `@packages/docs/src/model/document.ts`:
- Around line 351-368: The searchText method needs guards for
invalid/zero-length regexes: wrap the RegExp(query, flags) call in a try-catch
and return [] on construction errors to avoid throwing for inputs like "("; also
before the while ((match = pattern.exec(text)) !== null) loop reset
pattern.lastIndex = 0 and detect zero-length matches (match[0].length === 0) and
advance pattern.lastIndex (or break/skip) to prevent an infinite loop; update
references to pattern, pattern.exec, and searchText to implement these checks.
In `@packages/docs/src/view/clipboard.ts`:
- Around line 58-76: The node-walking logic currently only returns inline text,
so block-level elements and <br> are lost; update the walker in clipboard.ts
(the code handling node, Element, tag, TAG_STYLE_MAP and InlineStyle) to emit
explicit separators for block boundaries: when tag === 'br' emit a newline text
node (or an inline token representing a line break), and for block tags (p, div,
li, ul/ol boundaries, headings, blockquote, pre, etc.) ensure you insert a
paragraph/line-separator (e.g., '\n' or a sentinel token) before/after the block
so paragraphs and list items are preserved; also apply the same change around
the other occurrence noted (lines ~106-108) so both traversal branches preserve
these separators. Ensure callers still receive the flat inline list but with
separator tokens that represent original HTML boundaries.
- Around line 86-90: The font-size parser in clipboard.ts currently only matches
"NNpx" and assigns the raw number to style.fontSize, which ignores document
units (points) and drops "pt" values; update the logic that reads
el.style.fontSize so it accepts both "px" and "pt" (e.g. regex capturing value
and unit), then when unit === "px" convert to the model's point unit (points =
px * 72 / 96, i.e. px * 0.75) and when unit === "pt" use the parsed value
directly, and finally assign the computed point value to style.fontSize
(referencing el.style.fontSize and the InlineStyle variable style.fontSize).
- Around line 14-18: The deserializeBlocks function currently calls JSON.parse
directly which throws on malformed input; wrap the parse in a try-catch inside
deserializeBlocks, validate that the parsed object matches the expected
ClipboardPayload shape (e.g., has a numeric version and an array blocks), and if
parsing or validation fails return an empty array (and optionally log the error)
so callers like handlePaste can gracefully fall back instead of crashing.
In `@packages/docs/src/view/editor.ts`:
- Around line 710-727: The removeLink handler currently only clears href on the
single inline under the cursor (in removeLink), leaving adjacent runs of the
same logical link still clickable; change it to expand the selection left and
right across adjacent inlines in the same block that share the same
inline.style.href value before calling docStore.snapshot() and
doc.applyInlineStyle(). Specifically, locate the block via
doc.document.blocks.find(...) and, once you identify the inline under the cursor
(where inline.style.href is truthy), walk backwards and forwards from that
inline index while neighboring inlines have the exact same href, compute the
expanded anchor and focus offsets (use the block.id and cumulative offsets),
then call docStore.snapshot(), doc.applyInlineStyle(expandedRange, { href:
undefined }), followed by markDirty(block.id) and render().
- Around line 309-320: The search highlight path builds pseudo-selection
endpoints from bare {blockId, offset} and therefore needs explicit wrap affinity
to avoid painting on the wrong visual line; modify the mapping that produces
searchHighlightRects so each endpoint includes a lineAffinity (e.g.,
startAffinity/endAffinity or anchor/focus.affinity) derived the same way
selection handling does, then pass those affinity-aware endpoint objects into
computeSelectionRects (the call site using searchMatches, paginatedLayout,
layout, docCanvas.getContext(), canvasWidth) so computeSelectionRects receives
and uses lineAffinity for wrap-boundary disambiguation.
In `@packages/docs/src/view/find-replace.ts`:
- Around line 19-23: search() currently always sets activeIndex to 0 which
causes repeated replace operations to jump back to the first match; update
search(query, options?) so it preserves the existing activeIndex when the query
(and options) are unchanged: if this.query === query (and shallow-equal
this.options to options) keep this.activeIndex as-is (clamped to the new matches
length), otherwise set activeIndex = this.matches.length > 0 ? 0 : -1; use the
existing this.matches/doc.searchText and ensure replaceActive() can rely on
preserved activeIndex to advance to the next match instead of restarting at 0.
- Around line 46-57: The replace operations (replaceActive and the replace-all
path) mutate the Doc directly via this.doc.deleteText / this.doc.insertText
without establishing an undo boundary, so create an undo snapshot/transaction on
this.doc before performing the delete/insert and close/commit it after (i.e.,
call your project's Doc undo API such as begin/commitSnapshot or
createSnapshot/pushUndoBoundary around the deleteText/insertText calls in
replaceActive and the replace-all code paths), then call this.search(this.query,
this.options) as before; this ensures each replace or replace-all is a single
undoable operation.
In `@packages/docs/src/view/text-editor.ts`:
- Around line 1413-1443: The paste logic preserves inlines but drops block-level
metadata for the first and last pasted blocks; to fix it, when handling the head
and tail merges in the multi-block branch (after splitBlock and before calling
updateBlockDirect), copy the block-level fields (type, style, headingLevel,
listKind, listLevel, any other block-level properties present on Block) from
blocks[0] into headBlock and from blocks[blocks.length-1] into tailBlock (while
keeping head/tail ids and merging/overwriting their style/inlines as currently
done). Use existing helpers (spliceInlinesAt, getBlock/getBlockIndex,
updateBlockDirect) and keep the current middle-block creation
(generateBlockId/insertBlockAt) as-is; just set the metadata on headBlock and
tailBlock before the updateBlockDirect calls so two-block pastes also preserve
block-level formatting.
- Around line 509-516: The format-painter branch mutates inline styles directly
without creating an undo snapshot or marking affected layout as dirty; fix by
calling this.saveSnapshot() before this.doc.applyInlineStyle(...) and after
applying the style call the editor/document dirtying API to invalidate layout
for the affected selection (e.g., this.doc.markRangeDirty(this.selection.range)
or this.markBlocksDirtyForRange(this.selection.range) depending on the existing
API), then call this.requestRender(); this ensures the change is undoable and
forces remeasurement/re-render.
- Around line 519-529: The current inline
navigator.clipboard.readText().then(...) in the key handling block can reject
and leaves an unhandled promise; extract the logic into an async helper (e.g.,
private async pastePlainTextFromClipboard()) that wraps await
navigator.clipboard.readText() in try/catch, keeps the existing sequence
(this.saveSnapshot(), this.deleteSelection(), this.insertPlainText(text),
this.selection.setRange(null), this.requestRender()) and call that helper from
the key handler instead of using .then(); this replaces the .then() chain with
async/await and ensures failures are caught (optionally log or silently ignore
in the catch).
- Around line 624-629: The Ctrl/Cmd+click handler currently opens raw hrefs from
getLinkHrefAtMouse without validating the scheme; add a safety check (e.g.,
implement a private isSafeLinkHref(href: string) that constructs a URL relative
to window.location and only allows protocols 'http:', 'https:', 'mailto:',
'tel:') and call it before calling window.open in the Ctrl/Cmd click branch
inside the click handler where getLinkHrefAtMouse is used; if isSafeLinkHref
returns false, do not call window.open (and keep the default behavior).
In `@packages/docs/src/view/url-detect.ts`:
- Around line 12-20: The detectUrlBeforeCursor function is currently returning
the whole whitespace-delimited token (variable token) as the URL; instead locate
the scheme inside that token (search for "http://" or "https://") and extract
only the substring from the scheme start to the URL end, trimming trailing
sentence punctuation/closing delimiters (e.g., '.', ',', ';', ':', ')', ']',
'}', '"', ''') but not internal punctuation; compute and return start and end
indices based on the scheme start and trimmed end (instead of cursor token
boundaries) so hrefs like "https://example.com)" become "https://example.com";
add a regression test covering parenthesized and punctuation-terminated URLs to
verify correct trimming.
In `@packages/frontend/src/app/docs/docs-find-bar.tsx`:
- Around line 39-46: The component currently constructs mutable FindReplaceState
with stateRef.current = new FindReplaceState(editor.getDoc()) and allows
replace/replace-all to mutate the Doc directly; instead, keep FindReplaceState
read-only (do not call its mutation methods) and route all replacement actions
through the store-backed editor command layer (e.g., dispatch the store action
or call the editor/store replace methods you already have) so replacements go
through undo/sync paths; update the handlers that call replace/replace-all to
use the store/editor command API rather than FindReplaceState's mutators and
ensure stateRef.current is only used for read-only search metadata.
- Around line 62-76: Wrap the call to state.search inside runSearch in a
try-catch to guard regex mode against invalid patterns: call state.search(q, {
caseSensitive, useRegex }) inside try, call syncHighlights() on success, and in
catch clear highlights and set an inline validation/error state (e.g.
setRegexError or existing validation state) with the regex error message so the
component remains interactive instead of throwing; also ensure the useEffect
that re-runs runSearch([caseSensitive, useRegex]) will respect/clear that
validation state when options change or when the query becomes valid.
In `@packages/frontend/src/app/docs/docs-link-popover.tsx`:
- Around line 128-133: The code currently persists editUrl.trim() verbatim in
handleApply (and the similar handler around insertLink at lines ~173-179);
update these handlers to run the user-entered URL through a shared allow-listed
normalizer/sanitizer before calling editor.insertLink and before persisting or
rendering links. Implement or reuse a utility (e.g., normalizeSafeUrl(url):
string|null) that enforces allowed schemes (https, http, mailto, tel, etc.),
adds missing protocol (https) for bare hostnames if desired, and returns null
for disallowed schemes (javascript:, data:, etc.), then: 1) call
normalizeSafeUrl(editUrl.trim()) and abort with a user-safe error if it returns
null, 2) pass the normalized result to editor.insertLink and any persistence
paths, and 3) ensure the same normalizer is used by the HTML-paste path to
sanitize pasted <a href> values before insertion.
---
Nitpick comments:
In `@packages/docs/src/model/document.ts`:
- Around line 331-345: The methods updateBlockDirect and insertBlockAt perform
direct mutations on this.store without creating an undo snapshot, so callers
must establish an undo boundary before invoking them; add JSDoc above each
method (updateBlockDirect(blockId: string, block: Block): void and
insertBlockAt(index: number, block: Block): void) explicitly stating that they
do not snapshot state, mutate the store in-place, and require the caller to
create an undo boundary (e.g., via the surrounding handlePaste logic or a
start/commit undo API) before calling to avoid breaking undo/redo behavior.
In `@packages/docs/src/view/layout.ts`:
- Around line 256-259: Extract the magic 0.6 multiplier into a single exported
constant (e.g., SUPERSCRIPT_SCALE = 0.6) in your theme module and import it
here; replace the inline 0.6 used when computing charFontSize (the ternary using
seg.style.superscript || seg.style.subscript and Theme.defaultFontSize) with
SUPERSCRIPT_SCALE so all files (layout.ts, selection.ts, peer-cursor.ts,
doc-canvas.ts) can reuse the same value.
In `@packages/docs/test/model/types.test.ts`:
- Around line 135-143: Add a test that asserts inlineStylesEqual returns false
for two different href values: inside the existing "should detect href
difference" test (or as a new it block) call inlineStylesEqual({ href:
'https://example.com' }, { href: 'https://other.com' }) and expect(false); this
uses the inlineStylesEqual function to verify differing href strings are
considered unequal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d8606fd7-9d58-4c96-9906-e80040a8bd3b
📒 Files selected for processing (27)
docs/design/docs-wordprocessor-roadmap.mddocs/superpowers/plans/2026-03-25-docs-block-types.mddocs/superpowers/specs/2026-03-26-docs-phase2-inline-clipboard-design.mddocs/tasks/active/20260326-docs-phase2-todo.mdpackages/docs/src/index.tspackages/docs/src/model/document.tspackages/docs/src/model/types.tspackages/docs/src/view/clipboard.tspackages/docs/src/view/doc-canvas.tspackages/docs/src/view/editor.tspackages/docs/src/view/find-replace.tspackages/docs/src/view/layout.tspackages/docs/src/view/peer-cursor.tspackages/docs/src/view/selection.tspackages/docs/src/view/text-editor.tspackages/docs/src/view/url-detect.tspackages/docs/test/model/document.test.tspackages/docs/test/model/types.test.tspackages/docs/test/view/clipboard.test.tspackages/docs/test/view/find-replace.test.tspackages/docs/test/view/layout.test.tspackages/docs/test/view/url-detect.test.tspackages/frontend/src/app/docs/docs-find-bar.tsxpackages/frontend/src/app/docs/docs-formatting-toolbar.tsxpackages/frontend/src/app/docs/docs-link-popover.tsxpackages/frontend/src/app/docs/docs-view.tsxpackages/frontend/src/app/docs/yorkie-doc-store.ts
| # Docs Phase 2: Inline Extensions & Clipboard — Implementation Plan | ||
|
|
||
| > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | ||
|
|
||
| **Goal:** Add superscript/subscript, hyperlinks, clipboard operations, and find & replace to the Docs word processor. | ||
|
|
||
| **Architecture:** Extend the existing `InlineStyle` type with three new optional fields (`superscript`, `subscript`, `href`). Each feature follows the same vertical slice: data model → layout/rendering → shortcuts/toolbar → Yorkie serialization. Clipboard and Find & Replace are independent modules layered on top. | ||
|
|
||
| **Tech Stack:** TypeScript, Vitest, Canvas API, Yorkie CRDT | ||
|
|
||
| **Spec:** `docs/superpowers/specs/2026-03-26-docs-phase2-inline-clipboard-design.md` |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd '20260326-docs-phase2-(todo|lessons)\.md' docs/tasks/active -aRepository: wafflebase/wafflebase
Length of output: 130
Create the paired -lessons file.
This non-trivial task requires docs/tasks/active/20260326-docs-phase2-lessons.md to exist alongside the -todo file for tracking implementation learnings, per coding guidelines.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/tasks/active/20260326-docs-phase2-todo.md` around lines 1 - 11, The docs
task is missing its paired lessons file; create
docs/tasks/active/20260326-docs-phase2-lessons.md alongside the existing
20260326-docs-phase2-todo.md, include a matching title/metadata referencing the
Todo entry, a short summary of implementation learnings, a lessons checklist
template (what went well, blockers, follow-ups), and links back to the spec
docs/superpowers/specs/2026-03-26-docs-phase2-inline-clipboard-design.md so the
task and lessons remain linked for tracking and future retrospectives.
| // Scan backward from cursorOffset to find word start | ||
| let start = cursorOffset; | ||
| while (start > 0 && text[start - 1] !== ' ' && text[start - 1] !== '\n') { | ||
| start--; | ||
| } | ||
|
|
||
| const token = text.slice(start, cursorOffset); | ||
| if (/^https?:\/\/.+/.test(token)) { | ||
| return { start, end: cursorOffset, url: token }; |
There was a problem hiding this comment.
Don’t treat the entire whitespace token as the URL.
Right now detectUrlBeforeCursor() promotes the whole token. That stores broken hrefs for very common inputs like https://example.com) or https://example.com., and it misses parenthesized/quoted URLs because the scheme is no longer at the token start. Please extract the URL substring itself rather than assuming the whole token is link text, and add a regression around sentence-ending punctuation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/docs/src/view/url-detect.ts` around lines 12 - 20, The
detectUrlBeforeCursor function is currently returning the whole
whitespace-delimited token (variable token) as the URL; instead locate the
scheme inside that token (search for "http://" or "https://") and extract only
the substring from the scheme start to the URL end, trimming trailing sentence
punctuation/closing delimiters (e.g., '.', ',', ';', ':', ')', ']', '}', '"',
''') but not internal punctuation; compute and return start and end indices
based on the scheme start and trimmed end (instead of cursor token boundaries)
so hrefs like "https://example.com)" become "https://example.com"; add a
regression test covering parenthesized and punctuation-terminated URLs to verify
correct trimming.
| // Build / rebuild FindReplaceState when editor changes | ||
| useEffect(() => { | ||
| if (!editor) { | ||
| stateRef.current = null; | ||
| return; | ||
| } | ||
| stateRef.current = new FindReplaceState(editor.getDoc()); | ||
| }, [editor]); |
There was a problem hiding this comment.
Route replace mutations through the editor/store layer.
This component builds FindReplaceState from editor.getDoc(), and the replacement methods behind it mutate that Doc directly. That sidesteps the store-backed mutation path, so replace/replace-all can miss undo snapshots and Yorkie persistence/sync. Please keep the find state read-only and perform replacements through store-backed editor commands instead.
Also applies to: 97-110
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/frontend/src/app/docs/docs-find-bar.tsx` around lines 39 - 46, The
component currently constructs mutable FindReplaceState with stateRef.current =
new FindReplaceState(editor.getDoc()) and allows replace/replace-all to mutate
the Doc directly; instead, keep FindReplaceState read-only (do not call its
mutation methods) and route all replacement actions through the store-backed
editor command layer (e.g., dispatch the store action or call the editor/store
replace methods you already have) so replacements go through undo/sync paths;
update the handlers that call replace/replace-all to use the store/editor
command API rather than FindReplaceState's mutators and ensure stateRef.current
is only used for read-only search metadata.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
packages/docs/src/view/editor.ts (2)
310-320:⚠️ Potential issue | 🟡 MinorSearch highlights still drop wrap-boundary affinity.
These synthetic ranges are still built from bare
{ blockId, offset }endpoints. A match that starts or ends exactly at a visual wrap boundary can paint on the wrong line becausecomputeSelectionRects()has nolineAffinityto disambiguate it. Based on learningsIn packages/docs/src/view/*, when computing selection boundaries and wrap-aware cursor/navigation behavior, include and consistently thread lineAffinity through the relevant selection/text-editor helpers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/editor.ts` around lines 310 - 320, The search highlight ranges are built with plain endpoints (blockId/offset) which lose wrap-boundary affinity and can render on the wrong line; update the construction of the synthetic match endpoints used for computeSelectionRects (where searchHighlightRects is populated from searchMatches) to include a lineAffinity property on both anchor and focus (e.g., { blockId, offset, lineAffinity }) and ensure computeSelectionRects and any intermediate helpers accept and thread this lineAffinity through so wrap-aware rendering/disambiguation is preserved for boundary matches.
713-730:⚠️ Potential issue | 🟠 MajorStill only unlinks the current inline segment.
A single logical link can span adjacent inlines once formatting splits the text. This still clears
hrefonly on the inline under the cursor, so “Remove link” can leave the rest of the same link clickable. Expand left/right across contiguous inlines with the samehrefbefore callingapplyInlineStyle().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/editor.ts` around lines 713 - 730, The removeLink handler currently clears href only for the inline under the cursor; broaden the range by scanning adjacent inlines with the same href before calling applyInlineStyle. Locate removeLink and, when you detect inline.style.href, record the inline's href, then walk left from the current inline index while previousInline.style.href === href (accumulating start offset) and walk right while nextInline.style.href === href (accumulating end offset) to compute the full anchor/focus offsets, then call docStore.snapshot(); doc.applyInlineStyle(range, { href: undefined }); markDirty(block.id); render(); so the entire contiguous linked segment is unlinked.
🧹 Nitpick comments (1)
packages/docs/src/view/editor.ts (1)
782-790: Search-state updates can repaint without recomputing layout.
searchMatchesandactiveMatchIndexonly affect highlight paint, but both setters callrender(). Switching these torenderPaintOnly()avoids a full layout pass on every find-bar keystroke and match navigation.♻️ Suggested refactor
setSearchMatches: (matches: SearchMatch[], activeIndex: number) => { searchMatches = matches; activeMatchIndex = activeIndex; - render(); + renderPaintOnly(); }, clearSearchMatches: () => { searchMatches = []; activeMatchIndex = -1; - render(); + renderPaintOnly(); },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/editor.ts` around lines 782 - 790, The setters setSearchMatches and clearSearchMatches currently call render() causing a full layout on every search change; change both to call renderPaintOnly() instead (since searchMatches and activeMatchIndex only affect paint/highlights) so updates trigger paint-only repaints; update references in setSearchMatches and clearSearchMatches to use renderPaintOnly() while leaving searchMatches and activeMatchIndex assignment logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/docs/src/view/editor.ts`:
- Around line 736-749: getLinkAtCursor currently uses "if
(cursor.position.offset <= inlineEnd)" which treats a caret at an inline
boundary as part of the previous inline; change the boundary check to a
half-open range so the caret at the first character of the next inline resolves
to that inline. Update the condition in getLinkAtCursor to check
"cursor.position.offset >= pos && cursor.position.offset < inlineEnd" (or at
minimum "cursor.position.offset < inlineEnd") when iterating block.inlines so
the function returns the correct inline.style.href for boundary positions.
In `@packages/frontend/src/app/docs/docs-view.tsx`:
- Around line 335-345: The DocsLinkPopover receives editorRef.current which is
assigned after initialize() but doesn't trigger a re-render, so its
useEffect([editor]) (and the onCursorLinkChange subscription) is missed; make
the prop reactive by introducing a state variable (e.g., editorState) and
updating it when you set editorRef.current inside initialize() (or after the ref
write), then pass that state (not editorRef.current) to DocsLinkPopover;
alternatively you can force a remount by giving DocsLinkPopover a key tied to
editorRef.current, but the preferred fix is to setEditor(editorRef.current) in
initialize() and use editorState in the JSX so useEffect([editor]) inside
DocsLinkPopover sees the editor on first load.
---
Duplicate comments:
In `@packages/docs/src/view/editor.ts`:
- Around line 310-320: The search highlight ranges are built with plain
endpoints (blockId/offset) which lose wrap-boundary affinity and can render on
the wrong line; update the construction of the synthetic match endpoints used
for computeSelectionRects (where searchHighlightRects is populated from
searchMatches) to include a lineAffinity property on both anchor and focus
(e.g., { blockId, offset, lineAffinity }) and ensure computeSelectionRects and
any intermediate helpers accept and thread this lineAffinity through so
wrap-aware rendering/disambiguation is preserved for boundary matches.
- Around line 713-730: The removeLink handler currently clears href only for the
inline under the cursor; broaden the range by scanning adjacent inlines with the
same href before calling applyInlineStyle. Locate removeLink and, when you
detect inline.style.href, record the inline's href, then walk left from the
current inline index while previousInline.style.href === href (accumulating
start offset) and walk right while nextInline.style.href === href (accumulating
end offset) to compute the full anchor/focus offsets, then call
docStore.snapshot(); doc.applyInlineStyle(range, { href: undefined });
markDirty(block.id); render(); so the entire contiguous linked segment is
unlinked.
---
Nitpick comments:
In `@packages/docs/src/view/editor.ts`:
- Around line 782-790: The setters setSearchMatches and clearSearchMatches
currently call render() causing a full layout on every search change; change
both to call renderPaintOnly() instead (since searchMatches and activeMatchIndex
only affect paint/highlights) so updates trigger paint-only repaints; update
references in setSearchMatches and clearSearchMatches to use renderPaintOnly()
while leaving searchMatches and activeMatchIndex assignment logic unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6828e31a-3256-482d-9edf-8b27629d6b9b
📒 Files selected for processing (5)
packages/docs/src/view/doc-canvas.tspackages/docs/src/view/editor.tspackages/docs/src/view/text-editor.tspackages/docs/test/view/layout.test.tspackages/frontend/src/app/docs/docs-view.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/docs/test/view/layout.test.ts
- packages/docs/src/view/text-editor.ts
- Scroll active search match into view on Next/Previous navigation - Move cursor and select the active match text on Esc close - Matches Google Docs find behavior Co-Authored-By: Claude Opus 4.6 <[email protected]>
1875258 to
76c9893
Compare
Security (Critical): - Add URL scheme validation (isSafeUrl) before window.open - Add URL normalization (normalizeLinkUrl) for link insertion - Sanitize href in link popover view mode Robustness (Major): - Guard regex search against invalid patterns and zero-length matches - Add try-catch to deserializeBlocks for malformed JSON - Add undo snapshot to find-replace operations - Preserve activeIndex after replace instead of resetting to 0 - Add snapshot and dirty marking to format painter - Handle clipboard.readText() rejection with async/await - Expand removeLink across adjacent inlines with same href - Fix getLinkAtCursor boundary detection UI: - Fix find bar scroll-with-content via Portal + position:fixed - Unify find bar theme with shadcn design tokens Co-Authored-By: Claude Opus 4.6 <[email protected]>
6e59df3 to
635596b
Compare
- Preserve HTML block separators (<br>, <p>, <div>, etc.) during paste by emitting newline tokens and splitting into multiple blocks - Convert pasted CSS font-size px→pt (model uses points) - Trim trailing punctuation from auto-detected URLs - Preserve block-level metadata (type, style, heading, list) for head/tail blocks in multi-block paste - Make editor prop reactive via useState so DocsLinkPopover and DocsFindBar receive the editor on first render Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
packages/docs/src/view/editor.ts (1)
313-320:⚠️ Potential issue | 🟡 MinorSearch matches still lose wrap-boundary affinity.
These pseudo-ranges are still built from bare offsets before
computeSelectionRects(). A match that starts or ends exactly at a visual wrap can still highlight and scroll on the wrong line; carry explicit start/end affinity through this path.Based on learnings
In packages/docs/src/view/*, when computing selection boundaries and wrap-aware cursor/navigation behavior, include and consistently thread lineAffinity through the relevant selection/text-editor helpers.Also applies to: 809-815
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/editor.ts` around lines 313 - 320, The search match pseudo-ranges are being created from bare offsets so wrap-boundary affinity is lost; update the code that builds matches for searchHighlightRects (the searchMatches.map block that calls computeSelectionRects) to include explicit start/end (or anchor/focus) lineAffinity values on the selection objects and ensure computeSelectionRects (and the analogous creation at lines 809-815) accepts and threads those affinity properties through the selection/text-editor helpers so wrap-aware highlighting and scrolling use the provided affinities.packages/docs/src/view/clipboard.ts (1)
88-94:⚠️ Potential issue | 🟠 MajorSanitize pasted
hrefs before they enter the model.This assigns raw anchor targets straight to
InlineStyle.href. Unsafe schemes are blocked later when opening, but the value is still persisted and synced here. Filter importedhrefs through the shared allowlist up front.Possible fix
import type { Block, Inline, InlineStyle } from '../model/types.js'; import { inlineStylesEqual } from '../model/types.js'; +import { isSafeUrl } from './url-detect.js'; @@ if (tag === 'a') { const href = el.getAttribute('href'); - if (href) { + if (href && isSafeUrl(href)) { style.href = href; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/clipboard.ts` around lines 88 - 94, The code assigns raw anchor hrefs directly to InlineStyle.href (inside the tag === 'a' branch) which persists unsafe schemes; update this to pass el.getAttribute('href') through the shared allowlist/sanitizer module (the project's canonical href filter, e.g., isHrefAllowed/sanitizeHref) and only set style.href when the sanitizer returns an allowed/sanitized value (otherwise leave undefined/null); locate the anchor handling in clipboard.ts (the tag === 'a' block using el and style.href) and replace the direct assignment with a call to the shared allowlist API so pasted hrefs are filtered before entering the model.packages/docs/src/view/url-detect.ts (1)
49-57:⚠️ Potential issue | 🟠 MajorAuto-link still misses parenthesized and quoted URLs.
detectUrlBeforeCursor()still requires the whole token to start withhttp, so(https://example.com)and quoted URLs never link. The blanket closing-delimiter trim also chops valid URLs that legitimately end with a balanced). Search for the scheme inside the token and only strip unmatched trailing punctuation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/url-detect.ts` around lines 49 - 57, detectUrlBeforeCursor() currently assumes the token starts with the scheme and trims any trailing delimiter chars blindly; instead locate the first occurrence of /https?:\/\// inside the token (use the token variable) and set url = token.slice(schemeIndex) so parenthesized/quoted prefixes are handled. When trimming trailing punctuation from url, only strip closing delimiters that are unmatched: iterate removing trailing chars from url while keeping a balance count for parentheses, square brackets and braces and ignoring balanced trailing quotes/quote-pairs, so you don't remove legitimately balanced closing ')' or '"' at the end of a URL. After extraction ensure url.length > 'https://'.length and return { start, end: start + url.length, url } from detectUrlBeforeCursor().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/docs/src/view/clipboard.ts`:
- Around line 47-53: parseHtmlToInlines currently flattens all HTML into inline
runs and newlines so block-level semantics (h1–h6, p, li, blockquote, pre,
ul/ol) are lost; change parseHtmlToInlines to preserve block information by
either augmenting the Inline type with block metadata (e.g., blockType,
headingLevel, listType/listLevel, blockAttrs) or by returning a small wrapper
(e.g., Block[] or Inline|BlockBoundary tokens) that indicates block boundaries
and types; update the DOM walk in parseHtmlToInlines to detect block elements
(h1..h6, p, li, ul, ol, blockquote, pre, etc.) and emit the corresponding block
markers/metadata so packages/docs/src/view/text-editor.ts can reconstruct
original block structure and attributes.
In `@packages/docs/src/view/text-editor.ts`:
- Around line 1432-1451: The insertPlainText flow is treating every block as
editable text (using this.doc.insertText, this.doc.splitBlock and cursor moves)
which breaks for non-text blocks like horizontal-rule; add a reusable guard
helper (e.g., ensureEditableTextBlock or isTextBlock) and call it at the start
of insertPlainText and at the single-block rich-paste / no-selection link-insert
path so that when the current block is not a text block you first replace or
split into a new paragraph block (createParagraphBefore/After or call
this.doc.createBlock('paragraph') and move the cursor there) before calling
this.doc.insertText or this.doc.splitBlock, ensuring all insert/split logic only
runs on real text blocks; apply the same guard to the code around lines
1465-1475 and to the no-selection link insert code path so they reuse the
helper.
- Around line 633-655: The final unconditional blocks.push is creating a phantom
empty paragraph after the newline-splitting loop; change it so the trailing
block is only appended when needed by making the final push conditional (e.g.,
only push when current.length > 0 or when the original input explicitly requires
an empty paragraph) instead of always calling blocks.push(...). Update the code
around the inlines/parts loop that currently calls blocks.push({ id:
generateBlockId(), type: 'paragraph', inlines: current.length > 0 ? current : [{
text: '', style: {} }], style: { ...DEFAULT_BLOCK_STYLE } }) so it only executes
when current contains content or when an explicit empty-paragraph should be
preserved.
---
Duplicate comments:
In `@packages/docs/src/view/clipboard.ts`:
- Around line 88-94: The code assigns raw anchor hrefs directly to
InlineStyle.href (inside the tag === 'a' branch) which persists unsafe schemes;
update this to pass el.getAttribute('href') through the shared
allowlist/sanitizer module (the project's canonical href filter, e.g.,
isHrefAllowed/sanitizeHref) and only set style.href when the sanitizer returns
an allowed/sanitized value (otherwise leave undefined/null); locate the anchor
handling in clipboard.ts (the tag === 'a' block using el and style.href) and
replace the direct assignment with a call to the shared allowlist API so pasted
hrefs are filtered before entering the model.
In `@packages/docs/src/view/editor.ts`:
- Around line 313-320: The search match pseudo-ranges are being created from
bare offsets so wrap-boundary affinity is lost; update the code that builds
matches for searchHighlightRects (the searchMatches.map block that calls
computeSelectionRects) to include explicit start/end (or anchor/focus)
lineAffinity values on the selection objects and ensure computeSelectionRects
(and the analogous creation at lines 809-815) accepts and threads those affinity
properties through the selection/text-editor helpers so wrap-aware highlighting
and scrolling use the provided affinities.
In `@packages/docs/src/view/url-detect.ts`:
- Around line 49-57: detectUrlBeforeCursor() currently assumes the token starts
with the scheme and trims any trailing delimiter chars blindly; instead locate
the first occurrence of /https?:\/\// inside the token (use the token variable)
and set url = token.slice(schemeIndex) so parenthesized/quoted prefixes are
handled. When trimming trailing punctuation from url, only strip closing
delimiters that are unmatched: iterate removing trailing chars from url while
keeping a balance count for parentheses, square brackets and braces and ignoring
balanced trailing quotes/quote-pairs, so you don't remove legitimately balanced
closing ')' or '"' at the end of a URL. After extraction ensure url.length >
'https://'.length and return { start, end: start + url.length, url } from
detectUrlBeforeCursor().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f48eacbb-5bc4-48a8-8c6a-13e3dd8663d6
📒 Files selected for processing (10)
packages/docs/src/index.tspackages/docs/src/model/document.tspackages/docs/src/view/clipboard.tspackages/docs/src/view/editor.tspackages/docs/src/view/find-replace.tspackages/docs/src/view/text-editor.tspackages/docs/src/view/url-detect.tspackages/frontend/src/app/docs/docs-find-bar.tsxpackages/frontend/src/app/docs/docs-link-popover.tsxpackages/frontend/src/app/docs/docs-view.tsx
✅ Files skipped from review due to trivial changes (1)
- packages/docs/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/frontend/src/app/docs/docs-view.tsx
- packages/docs/src/view/find-replace.ts
- Rewrite parseHtmlToInlines → parseHtmlToBlocks returning Block[] with proper block semantics (headings, list items, blockquote) - Fix phantom empty paragraph from trailing block separators - Add ensureEditableBlock guard for horizontal-rule blocks - Remove unused DEFAULT_BLOCK_STYLE import - Create docs-phase2-lessons.md Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/docs/src/view/text-editor.ts (1)
1556-1570: Duplicate implementation of inline style comparison.
inlineStylesMatchduplicates the logic already available asinlineStylesEqualimported from../model/types.js(used inclipboard.ts). Consider reusing the shared utility for consistency and maintainability.Suggested fix
Add to imports at line 2:
-import { generateBlockId, getBlockText, getBlockTextLength } from '../model/types.js'; +import { generateBlockId, getBlockText, getBlockTextLength, inlineStylesEqual } from '../model/types.js';Then replace
inlineStylesMatchcalls withinlineStylesEqual:- if (last && this.inlineStylesMatch(last.style, inline.style)) { + if (last && inlineStylesEqual(last.style, inline.style)) {And remove the
inlineStylesMatchmethod (lines 1556-1570).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/text-editor.ts` around lines 1556 - 1570, Replace the duplicated inline style comparison by importing the shared utility inlineStylesEqual from ../model/types.js, update all usages of the local method inlineStylesMatch in this file to call inlineStylesEqual instead, and then delete the inlineStylesMatch method implementation (the private inlineStylesMatch function) to avoid duplication and keep consistency with clipboard.ts which already uses inlineStylesEqual.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/docs/src/view/clipboard.ts`:
- Around line 29-38: The TAG_STYLE_MAP constant is missing mappings for <sup>
and <sub>, so pasted HTML loses superscript/subscript formatting; add entries
"sup": { sup: true } and "sub": { sub: true } (or use the existing InlineStyle
property names used elsewhere for superscript/subscript) to TAG_STYLE_MAP so the
HTML parser maps those tags to the corresponding InlineStyle; update the
Record<string, Partial<InlineStyle>> in packages/docs/src/view/clipboard.ts
accordingly, using the exact InlineStyle property names your codebase expects
for superscript and subscript.
---
Nitpick comments:
In `@packages/docs/src/view/text-editor.ts`:
- Around line 1556-1570: Replace the duplicated inline style comparison by
importing the shared utility inlineStylesEqual from ../model/types.js, update
all usages of the local method inlineStylesMatch in this file to call
inlineStylesEqual instead, and then delete the inlineStylesMatch method
implementation (the private inlineStylesMatch function) to avoid duplication and
keep consistency with clipboard.ts which already uses inlineStylesEqual.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4d227df9-1407-4287-9843-5f9fbedae080
📒 Files selected for processing (3)
docs/tasks/active/20260326-docs-phase2-lessons.mdpackages/docs/src/view/clipboard.tspackages/docs/src/view/text-editor.ts
✅ Files skipped from review due to trivial changes (1)
- docs/tasks/active/20260326-docs-phase2-lessons.md
| const TAG_STYLE_MAP: Record<string, Partial<InlineStyle>> = { | ||
| b: { bold: true }, | ||
| strong: { bold: true }, | ||
| i: { italic: true }, | ||
| em: { italic: true }, | ||
| u: { underline: true }, | ||
| s: { strikethrough: true }, | ||
| del: { strikethrough: true }, | ||
| strike: { strikethrough: true }, | ||
| }; |
There was a problem hiding this comment.
Add <sup> and <sub> tag mappings for superscript/subscript.
The PR adds superscript/subscript support, but the HTML parser doesn't map <sup> and <sub> tags. Pasting HTML containing these elements will lose the formatting.
Proposed fix
const TAG_STYLE_MAP: Record<string, Partial<InlineStyle>> = {
b: { bold: true },
strong: { bold: true },
i: { italic: true },
em: { italic: true },
u: { underline: true },
s: { strikethrough: true },
del: { strikethrough: true },
strike: { strikethrough: true },
+ sup: { superscript: true },
+ sub: { subscript: true },
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const TAG_STYLE_MAP: Record<string, Partial<InlineStyle>> = { | |
| b: { bold: true }, | |
| strong: { bold: true }, | |
| i: { italic: true }, | |
| em: { italic: true }, | |
| u: { underline: true }, | |
| s: { strikethrough: true }, | |
| del: { strikethrough: true }, | |
| strike: { strikethrough: true }, | |
| }; | |
| const TAG_STYLE_MAP: Record<string, Partial<InlineStyle>> = { | |
| b: { bold: true }, | |
| strong: { bold: true }, | |
| i: { italic: true }, | |
| em: { italic: true }, | |
| u: { underline: true }, | |
| s: { strikethrough: true }, | |
| del: { strikethrough: true }, | |
| strike: { strikethrough: true }, | |
| sup: { superscript: true }, | |
| sub: { subscript: true }, | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/docs/src/view/clipboard.ts` around lines 29 - 38, The TAG_STYLE_MAP
constant is missing mappings for <sup> and <sub>, so pasted HTML loses
superscript/subscript formatting; add entries "sup": { sup: true } and "sub": {
sub: true } (or use the existing InlineStyle property names used elsewhere for
superscript/subscript) to TAG_STYLE_MAP so the HTML parser maps those tags to
the corresponding InlineStyle; update the Record<string, Partial<InlineStyle>>
in packages/docs/src/view/clipboard.ts accordingly, using the exact InlineStyle
property names your codebase expects for superscript and subscript.
Summary
Test plan
pnpm verify:fast— all tests pass🤖 Generated with Claude Code
Summary by CodeRabbit