From 60a18eff8a45eeb06bea3b9447bad8daf2972817 Mon Sep 17 00:00:00 2001 From: TumGovic Date: Thu, 4 Jun 2026 14:17:48 +0300 Subject: [PATCH] feat: add configurable paste mode setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Paste mode" setting in Appearance → Editing that controls how pasted text is handled: - Markdown (default): detects and renders markdown syntax as before - Plain text: inserts without any markdown processing - Code block: wraps the pasted content in a fenced ``` code block The setting persists to .scratch/settings.json alongside other editor settings. --- src/components/editor/Editor.tsx | 40 +++++++++++++++---- .../settings/EditorSettingsSection.tsx | 39 ++++++++++++++++++ src/context/ThemeContext.tsx | 23 +++++++++++ src/types/note.ts | 2 + 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/components/editor/Editor.tsx b/src/components/editor/Editor.tsx index dcf07161..611c9a58 100644 --- a/src/components/editor/Editor.tsx +++ b/src/components/editor/Editor.tsx @@ -546,7 +546,7 @@ export function Editor({ const pinNote = notesCtx?.pinNote; const unpinNote = notesCtx?.unpinNote; const notes = notesCtx?.notes; - const { textDirection } = useTheme(); + const { textDirection, pasteMode } = useTheme(); const [isSaving, setIsSaving] = useState(false); // Force re-render when selection changes to update toolbar active states const [, setSelectionKey] = useState(0); @@ -1213,22 +1213,46 @@ export function Editor({ } } - // Handle markdown text paste const text = clipboardData.getData("text/plain"); if (!text) return false; - // Check if text looks like markdown (has common markdown patterns) + const currentEditor = editorRef.current; + if (!currentEditor) return false; + + // Paste as plain text — strip all markdown formatting + if (pasteMode === "plain") { + currentEditor.commands.insertContent( + text.replace(/\r\n/g, "\n"), + { parseOptions: { preserveWhitespace: "full" } }, + ); + return true; + } + + // Paste as code block wrapped in ``` ``` + if (pasteMode === "code-block") { + const manager = currentEditor.storage.markdown?.manager; + if (manager && typeof manager.parse === "function") { + try { + const fenced = "```\n" + text + "\n```"; + const parsed = manager.parse(fenced); + if (parsed) { + currentEditor.commands.insertContent(parsed); + return true; + } + } catch { + // fall through to default + } + } + return false; + } + + // Default: "markdown" — detect and parse markdown patterns const markdownPatterns = /^#{1,6}\s|^\s*[-*+]\s|^\s*\d+\.\s|^\s*>\s|```|^\s*\[.*\]\(.*\)|^\s*!\[|\*\*.*\*\*|__.*__|~~.*~~|^\s*[-*_]{3,}\s*$|^\|.+\||\$\$[\s\S]+?\$\$/m; if (!markdownPatterns.test(text)) { - // Not markdown, let TipTap handle it normally return false; } - // Parse markdown and insert using editor ref - const currentEditor = editorRef.current; - if (!currentEditor) return false; - const manager = currentEditor.storage.markdown?.manager; if (manager && typeof manager.parse === "function") { try { diff --git a/src/components/settings/EditorSettingsSection.tsx b/src/components/settings/EditorSettingsSection.tsx index 997e9180..0e47c681 100644 --- a/src/components/settings/EditorSettingsSection.tsx +++ b/src/components/settings/EditorSettingsSection.tsx @@ -6,6 +6,7 @@ import type { TextDirection, EditorWidth, ThemeColorKey, + PasteMode, } from "../../types/note"; import { ChevronRightIcon, EyeIcon, MinusIcon, PlusIcon } from "../icons"; import { cn } from "../../lib/utils"; @@ -48,6 +49,13 @@ const fontFamilyOptions: { value: FontFamily; label: string }[] = [ { value: "monospace", label: "Mono" }, ]; +// Paste mode options +const pasteModeOptions: { value: PasteMode; label: string; description: string }[] = [ + { value: "markdown", label: "Markdown", description: "Parse and render markdown syntax" }, + { value: "plain", label: "Plain text", description: "Insert as plain text, no formatting" }, + { value: "code-block", label: "Code block", description: "Wrap in a ``` code block" }, +]; + // Bold weight options (medium excluded for monospace) const boldWeightOptions = [ { value: 500, label: "Medium", excludeForMonospace: true }, @@ -77,6 +85,8 @@ export function AppearanceSettingsSection() { setCustomColor, resetCustomColor, resetAllCustomColors, + pasteMode, + setPasteMode, } = useTheme(); // Validated numeric change handler @@ -458,6 +468,35 @@ export function AppearanceSettingsSection() {
+ + {/* Divider */} +
+ + {/* Editing Section */} +
+

Editing

+
+
+
+ + + {pasteModeOptions.find((o) => o.value === pasteMode)?.description} + +
+ +
+
+
); } diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx index f6b3472b..281a986f 100644 --- a/src/context/ThemeContext.tsx +++ b/src/context/ThemeContext.tsx @@ -16,6 +16,7 @@ import type { EditorWidth, CustomColors, ThemeColorKey, + PasteMode, } from "../types/note"; type ThemeMode = "light" | "dark" | "system"; @@ -117,6 +118,8 @@ interface ThemeContextType { setCustomColor: (mode: "light" | "dark", key: ThemeColorKey, value: string) => void; resetCustomColor: (mode: "light" | "dark", key: ThemeColorKey) => void; resetAllCustomColors: (mode: "light" | "dark") => void; + pasteMode: PasteMode; + setPasteMode: (mode: PasteMode) => void; } const ThemeContext = createContext(null); @@ -189,6 +192,7 @@ export function ThemeProvider({ children }: ThemeProviderProps) { ); const [customColorsLight, setCustomColorsLightState] = useState({}); const [customColorsDark, setCustomColorsDarkState] = useState({}); + const [pasteMode, setPasteModeState] = useState("markdown"); const [isInitialized, setIsInitialized] = useState(false); const [systemTheme, setSystemTheme] = useState<"light" | "dark">(() => { @@ -248,6 +252,13 @@ export function ThemeProvider({ children }: ThemeProviderProps) { if (settings.customColorsDark) { setCustomColorsDarkState(settings.customColorsDark); } + if ( + settings.pasteMode === "markdown" || + settings.pasteMode === "plain" || + settings.pasteMode === "code-block" + ) { + setPasteModeState(settings.pasteMode); + } } catch { // If settings can't be loaded, use defaults } @@ -544,6 +555,16 @@ export function ThemeProvider({ children }: ThemeProviderProps) { [], ); + const setPasteMode = useCallback(async (mode: PasteMode) => { + setPasteModeState(mode); + try { + const settings = await getSettings(); + await updateSettings({ ...settings, pasteMode: mode }); + } catch (error) { + console.error("Failed to save paste mode:", error); + } + }, []); + // Live CSS variable update during drag (no persistence) const setEditorMaxWidthLive = useCallback((value: string) => { document.documentElement.style.setProperty("--editor-max-width", value); @@ -579,6 +600,8 @@ export function ThemeProvider({ children }: ThemeProviderProps) { setCustomColor, resetCustomColor, resetAllCustomColors, + pasteMode, + setPasteMode, }} > {children} diff --git a/src/types/note.ts b/src/types/note.ts index 37addc1b..ecb8dd4c 100644 --- a/src/types/note.ts +++ b/src/types/note.ts @@ -20,6 +20,7 @@ export interface ThemeSettings { export type FontFamily = "system-sans" | "serif" | "monospace"; export type TextDirection = "auto" | "ltr" | "rtl"; export type EditorWidth = "narrow" | "normal" | "wide" | "full" | "custom"; +export type PasteMode = "markdown" | "plain" | "code-block"; export interface EditorFontSettings { baseFontFamily?: FontFamily; @@ -59,6 +60,7 @@ export interface Settings { ignoredPatterns?: string[]; customColorsLight?: CustomColors; customColorsDark?: CustomColors; + pasteMode?: PasteMode; } export interface FolderNode {