From 6f712bf43fc4573b6291ef8459a3f6fc5cee403b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:32:55 +0200 Subject: [PATCH] fix: compose editor inline script died on parse, keeping Send disabled #11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The template literal in buildEditorHtml cooked the backslash escapes in the insertLink scheme regex (\/ and \? became bare / and ?), so the page received /^(https?:|mailto:|tel:|sms:|/|?|#)/i — an invalid regex. The resulting SyntaxError killed the entire inline editor script: no change messages ever reached RN, bodyHtml stayed empty, canSend never enabled without an attachment, and mails sent with one had empty bodies. Regressed in 29b06a3. Rewrite the scheme check without backslash escapes and move the page builder into src/lib/editor-html.ts so a unit test can assert the emitted script parses and stays backslash-free. Verified on a Pixel 7 API 34 emulator (release build): typing now posts change messages and the plain-text body extraction sees the content. --- src/components/RichTextEditor.tsx | 276 +------------------------ src/lib/__tests__/editor-html.test.ts | 50 +++++ src/lib/editor-html.ts | 286 ++++++++++++++++++++++++++ 3 files changed, 339 insertions(+), 273 deletions(-) create mode 100644 src/lib/__tests__/editor-html.test.ts create mode 100644 src/lib/editor-html.ts diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx index 1ab257f9..146125cd 100644 --- a/src/components/RichTextEditor.tsx +++ b/src/components/RichTextEditor.tsx @@ -3,10 +3,7 @@ import { StyleSheet, View, Platform } from 'react-native'; import { WebView, type WebViewMessageEvent } from 'react-native-webview'; import { useColors } from '../theme/colors'; import type { ThemePalette } from '../theme/tokens'; - -// Minimum visible editor height (px). The editor auto-grows beyond this as the -// user types, and the parent ScrollView handles overflow. -const MIN_HEIGHT = 220; +import { buildEditorHtml, MIN_EDITOR_HEIGHT } from '../lib/editor-html'; export type RichTextCommand = | 'bold' @@ -66,273 +63,6 @@ interface Props { onBlur?: () => void; } -function buildEditorHtml(opts: { - initialHtml: string; - placeholder: string; - c: ThemePalette; -}): string { - const { initialHtml, placeholder, c } = opts; - // Inject initial content as a JSON-encoded string so any HTML/quotes inside - // are safely embedded (no template literal collision with the script body). - const initialJson = JSON.stringify(initialHtml); - const placeholderJson = JSON.stringify(placeholder); - - return ` - - - - - - - -
- - -`; -} - const RichTextEditor = React.forwardRef(function RichTextEditor( { initialHtml = '', placeholder = '', onChange, onSelectionChange, onFocus, onBlur }, ref, @@ -340,7 +70,7 @@ const RichTextEditor = React.forwardRef(function Ri const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); const webViewRef = React.useRef(null); - const [height, setHeight] = React.useState(MIN_HEIGHT); + const [height, setHeight] = React.useState(MIN_EDITOR_HEIGHT); const onChangeRef = React.useRef(onChange); const onSelectionRef = React.useRef(onSelectionChange); const onFocusRef = React.useRef(onFocus); @@ -400,7 +130,7 @@ const RichTextEditor = React.forwardRef(function Ri break; case 'height': { const h = typeof data.payload === 'number' ? data.payload : Number(data.payload); - if (!Number.isNaN(h) && h > 0) setHeight(Math.max(MIN_HEIGHT, h)); + if (!Number.isNaN(h) && h > 0) setHeight(Math.max(MIN_EDITOR_HEIGHT, h)); break; } case 'selection': diff --git a/src/lib/__tests__/editor-html.test.ts b/src/lib/__tests__/editor-html.test.ts new file mode 100644 index 00000000..b69f1808 --- /dev/null +++ b/src/lib/__tests__/editor-html.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { buildEditorHtml } from '../editor-html'; +import { LIGHT_COLORS, DARK_COLORS } from '../../theme/tokens'; + +function extractScript(html: string): string { + const m = html.match(/ + +`; +}