mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-11 08:42:45 +00:00
* fix(notes): domain paste policy + Codex review follow-ups Move clipboard paste resolution to domain/notes (pure, no React) and address post-#2868 review items: structured text/plain wins over wrapper HTML, caret-safe insert for long pastes (no EOF append), https/protocol-relative image src, entity decode, quote-aware img attrs, code-region protection, nested HTML islands, hard-break preservation, height-only image CSS, and complete small-icon widths. * fix: address Codex review on PR #2869 * perf(notes): cut hover re-renders and heavy image :has() CSS Reduce perceived lag in large notes by (1) coalescing link-hover setState with equality checks and rAF, (2) replacing the combinatorial :has(img[width]) matrix with data-note-img-size markers plus lazy images, and (3) merging DOM decoration into one observer that debounces in edit mode so Lexical keystrokes do not re-walk anchors every frame. * perf(notes): render preview with Streamdown instead of MDXEditor Vault note preview no longer mounts Lexical/MDXEditor/CodeMirror for read-only viewing. Preview uses static Streamdown (same stack as chat) with HTML islands for centered heroes and sized images; edit mode keeps MDXEditor. This cuts large-note scroll and mode-switch cost while preserving host-link click handling and compact badge image layout. * fix(notes): render markdown inside center HTML for Streamdown preview Streamdown/remark does not parse Markdown nested in HTML blocks, so README-style <div align="center"> heroes showed raw # ** and badge syntax. Preprocess preview markdown: expand centered islands via marked GFM to HTML, and rewrite relative images so Streamdown no longer shows "Image blocked" chips under app CSP. * fix(notes): GitHub-style preview with react-markdown Replace Streamdown note preview (chat-oriented Image blocked chrome and poor README layout) with react-markdown + remark-gfm + rehype-raw/sanitize and github-markdown-css. Center HTML islands still expand via marked so pasted heroes render; relative images and empty links are dropped quietly instead of alt-as-title or blocked chips. * fix(notes): keep relative images; map Vite public/ to site root Stop dropping non-https note images. Preserve absolute and relative src as-is, except rewrite repo public/ assets to site-root paths (public/icon.png → /icon.png) so Vite/Electron serve them without /public/* warnings. Unwrap oversized center blocks so Features-style body stays left-aligned in preview like edit mode. * fix(notes): left-align preview body; center only README hero Force markdown-body text-align left and direct block headings/lists left. After the first hero thematic break, strip align=center / text-align:center so Catty/Features sections no longer inherit full-width centering. Tighten oversized-center heuristics (Catty Agent, lists, blockquotes) without treating the Netcatty logo title as a body section. * fix(notes): force preview body left-align (nuclear CSS + unwrap) Catty/Features still rendered centered because small align=center shells and inheritance beat partial CSS. Force every node left via * selector, re-enable center only under [align=center], unwrap all center shells after the hero --- band, and set inline textAlign left on the preview root. * fix(notes): stop /public asset requests; fix preview center inheritance Root cause of body still centering: CSS re-applied text-align:center to all descendants of [align=center], so one large shell centered Catty/lists. Keep * left; only the shell itself may center (inline badges). Root cause of Vite /public/* warnings: stored notes still had /public/... paths and MDX loaded them raw. normalizeNotePublicAssetPaths rewrites /public/ and public/ to site-root paths for both edit and preview display. * revert(notes): restore MDXEditor for preview (drop GitHub markdown path) GitHub-style react-markdown preview caused layout and asset-path issues that diverged from edit mode. Preview again uses MDXEditor with readOnly + mode key remount, matching the pre-Streamdown/GitHub path. Keep display-time public/ → / rewrite so Vite does not request /public/*. * perf(notes): reuse MDX editor when switching notes Remove key={noteId} remount that tore down Lexical/CodeMirror on every note change. Pass noteId and call setMarkdown on switch instead, after the existing draft flush, so selection stays snappy and only document content is replaced. * perf(notes): defer setMarkdown on switch so selection paints first Yield a double rAF before Lexical import on note change, show a blocking overlay for large notes, and ignore external sync while a swap is in flight. Hardens flaky Suspense loading assertions in SSR tests. * fix(notes): align task-list checkboxes and make them clickable in preview Center GFM checkboxes on the first text line, give them a solid hit target, and toggle markdown from preview clicks since Lexical readOnly blocks CheckListPlugin. Pure toggle helper lives in domain/notes/taskList. * fix(notes): close multi-agent review findings on switch/paste/tasks Gate stale onChange during deferred note-switch import, keep draft refs in display-markdown space, baseline paste settle on live getMarkdown to avoid false success after muted setMarkdown, mask code regions in task toggles, and drop abandoned GitHub/Streamdown preview module and deps. * fix(notes): address remaining Codex review findings Clear Lexical undo history on note switch, always inert-block during deferred setMarkdown, re-annotate host links and image sizes on attribute mutations, harden entity/code-region paste paths, and angle-bracket image destinations that contain spaces. * fix(notes): tighten Codex follow-ups on src, code masks, tasks Prefer real img src over data-src, normalize public/ paths only outside code, mask indented/blockquote fences, and accept 1) ordered task markers. * fix(notes): keep swap payloads fresh; keep nested list tasks Refresh deferred setMarkdown when the same note's value changes mid-yield, and stop treating nested list lines as indented code so task toggles stay aligned with the preview DOM. * test(notes): update swap guard source assertions * fix(notes): collision-free code masks, real href, cancel hover rAF Use a document-unique mask sentinel, match anchor href with attribute boundaries (not data-href), and cancel the pending link-hover frame on mouse leave so chips cannot reappear after exit. * fix(notes): accept longer closing fences in code masks CommonMark allows closing fences with more backticks/tildes than the opener; line-scan masking now matches length >= open so public/ paths and task samples inside those blocks stay protected. * fix(notes): harden CommonMark fence, span, and indented-code masks Use homogeneous fence openers, multi-backtick inline spans, and list-context aware indented-code detection so samples stay masked without breaking nested task indexing. * fix(notes): task spacing, angled image dests, deep blockquote fences Require whitespace after task brackets, parse <angled> image destinations with spaces, and mask fenced code at any blockquote depth. * fix(notes): raw-text HTML elements and HTML comment task masks Locate script/style closers without nested-tag walks so raw bodies with '<' are removed, and mask HTML comments before task indexing so preview toggles hit visible checkboxes only. * perf(notes): drop fade animation on note switch and editor load Use an instant solid cover instead of opacity/transform fade-in so Lexical swaps do not pay extra composite cost that can feel more janky than a hard cut. --------- Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
190 lines
6.6 KiB
TypeScript
190 lines
6.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
const managerSource = readFileSync(new URL("./NotesManager.tsx", import.meta.url), "utf8");
|
|
const layoutSource = readFileSync(
|
|
new URL("../vault/VaultViewLayout.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
|
|
test("note content drafts stay in refs so MDX keystrokes do not rebuild the shell", () => {
|
|
assert.doesNotMatch(
|
|
managerSource,
|
|
/const \[draftContent, setDraftContent\]/,
|
|
"draftContent React state causes a full NotesManager render per keystroke",
|
|
);
|
|
assert.match(
|
|
managerSource,
|
|
/draftContentRef\.current = fields\.content;/,
|
|
);
|
|
assert.doesNotMatch(
|
|
managerSource,
|
|
/setDraftContent\(fields\.content\)/,
|
|
);
|
|
});
|
|
|
|
test("NotesManager teardown flush uses a stable ref under StrictMode", () => {
|
|
assert.match(managerSource, /flushNoteDraftRef\.current = flushNoteDraft/);
|
|
assert.match(
|
|
managerSource,
|
|
/useEffect\(\(\) => \(\) => \{\s*\n\s*flushNoteDraftRef\.current\(\);\s*\n\s*\}, \[\]\)/,
|
|
);
|
|
});
|
|
|
|
test("Vault notes section is memoized against unrelated VaultView churn", () => {
|
|
assert.match(layoutSource, /const MemoVaultNotesSection = React\.memo/);
|
|
assert.match(layoutSource, /<MemoVaultNotesSection\b/);
|
|
assert.match(layoutSource, /const handleNotesOpenHost = useCallback/);
|
|
assert.match(layoutSource, /onOpenHost=\{handleNotesOpenHost\}/);
|
|
assert.match(
|
|
layoutSource,
|
|
/useNotesStore\(\{\s*\n\s*enabled:\s*isActive,\s*\n\s*\}\)/,
|
|
);
|
|
assert.match(
|
|
layoutSource,
|
|
/if \(next\.isActive && prev\.hosts !== next\.hosts\) return false;/,
|
|
"hidden retained notes must ignore hosts identity churn",
|
|
);
|
|
});
|
|
|
|
test("hidden terminal notes side panel does not subscribe to notes publishes", () => {
|
|
const slotsSource = readFileSync(
|
|
new URL("../terminalLayer/terminalLayerSidePanelSlots.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(slotsSource, /useNotesStore\(\{\s*enabled:\s*isVisible\s*\}\)/);
|
|
});
|
|
|
|
test("notes manager prefetches the MDXEditor chunk when becoming active", () => {
|
|
assert.match(managerSource, /prefetchInlineMarkdownEditor/);
|
|
assert.match(
|
|
managerSource,
|
|
/if \(!isActive\) return;\s*\n\s*prefetchInlineMarkdownEditor\(\);/,
|
|
);
|
|
});
|
|
|
|
test("mode toggle flushes ref-only content drafts before remounting the editor", () => {
|
|
assert.match(
|
|
managerSource,
|
|
/flushNoteDraft\(\);\s*\n\s*setNoteEditorMode/,
|
|
"preview/edit remount must see the in-progress body",
|
|
);
|
|
});
|
|
|
|
test("note switches reuse MDX instance instead of key=noteId remount", () => {
|
|
assert.match(managerSource, /noteId=\{selectedNoteView\.id\}/);
|
|
assert.doesNotMatch(
|
|
managerSource,
|
|
/<InlineMarkdownEditor[\s\S]{0,200}key=\{selectedNoteView\.id\}/,
|
|
"key=noteId forces full Lexical teardown on every note switch",
|
|
);
|
|
const editorSource = readFileSync(
|
|
new URL("./InlineMarkdownEditor.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(editorSource, /noteId !== noteIdRef\.current/);
|
|
assert.match(editorSource, /setMarkdown\(scheduled\.markdown\)/);
|
|
assert.match(
|
|
editorSource,
|
|
/contentSwapFramesRef\.current\.outer = window\.requestAnimationFrame/,
|
|
"outer rAF yields a paint so the tree selection updates before Lexical import",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/contentSwapFramesRef\.current\.inner = window\.requestAnimationFrame/,
|
|
"inner rAF completes the double-yield before setMarkdown",
|
|
);
|
|
assert.match(editorSource, /data-notes-content-swapping="true"/);
|
|
assert.match(editorSource, /setIsContentSwapping\(true\)/);
|
|
assert.match(editorSource, /CLEAR_HISTORY_COMMAND/);
|
|
assert.match(editorSource, /clearLexicalHistory/);
|
|
assert.match(editorSource, /attributeFilter:\s*\[\s*"width",\s*"height"/);
|
|
assert.match(editorSource, /onPointerDownCapture=\{blockWhileContentSwapping\}/);
|
|
assert.match(editorSource, /startTransition\(\(\) => setIsContentSwapping\(false\)\)/);
|
|
assert.match(
|
|
editorSource,
|
|
/latestMarkdownRef\.current !== scheduled\.markdown/,
|
|
"deferred setMarkdown must not clobber edits typed after the switch",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/if \(contentSwapPendingRef\.current\) return;/,
|
|
"stale onChange during the swap yield must not write the previous note into the new draft",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/contentSwapScheduledRef/,
|
|
"deferred import must refresh when the same note's value changes during the yield",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/syncedPropValueRef\.current = markdown;/,
|
|
"draft-clobber guard compares display-normalized values after note switch",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/runDecorations\(true\)/,
|
|
"edit-mode note swaps must re-annotate host links after setMarkdown",
|
|
);
|
|
});
|
|
|
|
test("host-link annotation does not re-run on every markdown value keystroke", () => {
|
|
const editorSource = readFileSync(
|
|
new URL("./InlineMarkdownEditor.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.doesNotMatch(
|
|
editorSource,
|
|
/annotateHostLinks,\s*value\s*\]/,
|
|
"value in annotateHostLinks effect deps walks the DOM on every keystroke",
|
|
);
|
|
assert.doesNotMatch(
|
|
editorSource,
|
|
/annotateCodeBlockCopyButtons,\s*editorMode,\s*value\s*\]/,
|
|
"value in code-copy effect deps re-walks every code block on each draft identity change",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/\[annotateCodeBlockCopyButtons, annotateHostLinks, editorMode\]/,
|
|
"DOM decoration is independent of markdown value identity",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/readOnly=\{editorMode === "preview"\}/,
|
|
"preview reuses MDXEditor in read-only mode",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/syncedPropValueRef/,
|
|
"external note publishes must not clobber an in-progress local draft",
|
|
);
|
|
assert.match(
|
|
editorSource,
|
|
/latestMarkdownRef\.current !== syncedPropValueRef\.current/,
|
|
);
|
|
});
|
|
|
|
test("link hover and small-image CSS avoid render thrash", () => {
|
|
const editorSource = readFileSync(
|
|
new URL("./InlineMarkdownEditor.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const cssSource = readFileSync(new URL("../../index.css", import.meta.url), "utf8");
|
|
|
|
const imageLayoutSource = readFileSync(
|
|
new URL("./noteImageLayout.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(editorSource, /linkActionStatesEqual/);
|
|
assert.match(editorSource, /setLinkActionIfChanged/);
|
|
assert.match(editorSource, /annotateNoteImageSizes/);
|
|
assert.match(imageLayoutSource, /data-note-img-size/);
|
|
// No combinatorial :has(img[width="N"]) matrix for small icons.
|
|
assert.doesNotMatch(
|
|
cssSource,
|
|
/:has\(img\[width="16"\]\).*?:has\(img\[width="20"\]/s,
|
|
);
|
|
assert.match(cssSource, /img\[data-note-img-size="sm"\]/);
|
|
});
|