From 47a1d24999217c8544d83ff5edf89a0dcaa492da Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:25:57 +0100 Subject: [PATCH] fix: keep a quoted stylesheet from ending the composer's editor script --- src/components/RichTextEditor.tsx | 10 ++------ src/lib/__tests__/editor-html.test.ts | 35 +++++++++++++++++++++---- src/lib/__tests__/email-html.test.ts | 11 ++++++++ src/lib/editor-html.ts | 37 ++++++++++++++++++++++++--- src/lib/email-html.ts | 15 ++++++++++- 5 files changed, 90 insertions(+), 18 deletions(-) diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx index 3d1444ef..b961d380 100644 --- a/src/components/RichTextEditor.tsx +++ b/src/components/RichTextEditor.tsx @@ -4,7 +4,7 @@ import { WebView, type WebViewMessageEvent } from 'react-native-webview'; import { useColors } from '../theme/colors'; import type { ThemePalette } from '../theme/tokens'; import { perfEndAfterPaint } from '../lib/perf-marks'; -import { buildEditorHtml, MIN_EDITOR_HEIGHT } from '../lib/editor-html'; +import { buildEditorHtml, isEditorNavigationAllowed, MIN_EDITOR_HEIGHT } from '../lib/editor-html'; export type RichTextCommand = | 'bold' @@ -276,13 +276,7 @@ const RichTextEditor = React.forwardRef(function Ri androidLayerType={Platform.OS === 'android' ? 'hardware' : undefined} style={styles.webview} containerStyle={styles.webviewContainer} - onShouldStartLoadWithRequest={(request) => { - // Allow only the initial about:blank doc + data URIs we emit - // ourselves. Reject `about:*` (e.g. about:srcdoc) and every other - // scheme so a paste-injected anchor can't navigate the editor. - const url = request.url; - return !url || url === 'about:blank' || url.startsWith('data:'); - }} + onShouldStartLoadWithRequest={(request) => isEditorNavigationAllowed(request.url)} /> ); diff --git a/src/lib/__tests__/editor-html.test.ts b/src/lib/__tests__/editor-html.test.ts index a5f192b6..ab75c1aa 100644 --- a/src/lib/__tests__/editor-html.test.ts +++ b/src/lib/__tests__/editor-html.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { - buildEditorCsp, buildEditorHtml, CHANGE_THROTTLE_MS, shouldBlockEditorRemoteImages, + buildEditorCsp, buildEditorHtml, CHANGE_THROTTLE_MS, isEditorNavigationAllowed, shouldBlockEditorRemoteImages, } from '../editor-html'; import { LIGHT_COLORS, DARK_COLORS } from '../../theme/tokens'; @@ -28,18 +28,43 @@ describe('buildEditorHtml', () => { it('emits no cooked-away backslashes in the script', () => { // The page script is written escape-free on purpose; a backslash-free // source cannot silently lose escapes to template-literal cooking. - const script = extractScript(buildEditorHtml(base)); + // (The embedded content writes `<` as an escape, so it is left out here.) + const script = extractScript(buildEditorHtml({ ...base, initialHtml: 'x' })); expect(script).not.toContain('\\'); }); - it('embeds initial content and placeholder as JSON strings', () => { + it('embeds initial content as a JSON string and the placeholder as an attribute', () => { const html = buildEditorHtml({ ...base, initialHtml: '

He said "hi" & left

', placeholder: 'Say "something"…', }); - expect(html).toContain(JSON.stringify('

He said "hi" & left

')); - expect(html).toContain(`data-placeholder=${JSON.stringify('Say "something"…')}`); + expect(html).toContain(JSON.stringify('

He said "hi" & left

').replace(/ carried ``: the page script + // ended there, the rest of the reply ran as markup and a refresh meta took + // the editor to a data:text/html page. + it('keeps a quoted { + const quoted = '

hello

-
+