// Builds the self-contained HTML document for the RichTextEditor WebView.
// Kept as a pure module (no react-native imports) so tests can assert the
// embedded page script stays syntactically valid JavaScript.
//
// IMPORTANT: the page script lives inside a JS template literal. Backslash
// escapes are "cooked" by the template (`\/` becomes `/`, `\s` becomes `s`),
// which once shipped an invalid regex that killed the whole inline script
// with a SyntaxError (issue #11 — Send stayed disabled because the editor
// never posted `change` messages). Never rely on a backslash reaching the
// page verbatim; write escape-free code or double the backslash.
import type { ThemePalette } from '../theme/tokens';
import { randomHex } from './random';
// Minimum visible editor height (px). The editor auto-grows beyond this as the
// user types, and the parent ScrollView handles overflow.
export const MIN_EDITOR_HEIGHT = 220;
// Shortest gap between two `change` posts while typing (see the page script).
// Readers that need the current body - send, save, format switch - ask the
// page with `getHtml` instead of waiting for the next post.
export const CHANGE_THROTTLE_MS = 400;
/**
* The editor page's Content-Security-Policy, mirroring the viewer's
* (`src/lib/email-html.ts`). Nothing but images, styles and our own inline
* script may load. With `blockRemoteImages` images and fonts are limited to
* what the composer produces itself - `data:` (inline and hydrated `cid:`
* images) and `blob:` (pasted images) - so a quoted original's tracking
* pixels and remote images don't fire just because it is replied to.
*
* Scripts need the page's `nonce`: the editor's own script carries it, and
* inline handlers in seeded or pasted HTML (a quoted ``)
* don't run. Commands from the app arrive through injectJavaScript, which
* page CSP doesn't govern.
*/
export function buildEditorCsp(blockRemoteImages: boolean, nonce?: string): string {
const resources = blockRemoteImages
? ['img-src data: blob:', 'font-src data:', "media-src 'none'"]
: ['img-src data: blob: https: http:', 'font-src data: https: http:', 'media-src data: https: http:'];
return [
"default-src 'none'",
nonce ? `script-src 'nonce-${nonce}'` : "script-src 'none'",
"style-src 'unsafe-inline'",
...resources,
"base-uri 'none'",
"form-action 'none'",
].join('; ');
}
/**
* Whether the composer must open the editor with remote images blocked: it
* is seeded with someone else's HTML (the quoted original of a reply or
* forward, or a reopened draft that carries a quote) and the viewer would
* have shown that message with its remote content blocked too. A reopened
* draft doesn't say whose message it quotes, so its quote counts as
* untrusted.
*/
export function shouldBlockEditorRemoteImages(opts: {
/** The HTML the editor is seeded with from a message, if any. */
seedHtml?: string | null;
isDraft: boolean;
externalContentPolicy: 'allow' | 'block' | 'ask';
/** The quoted message's sender is trusted (ignored for drafts). */
senderTrusted: boolean;
}): boolean {
const { seedHtml, isDraft, externalContentPolicy, senderTrusted } = opts;
if (!seedHtml || externalContentPolicy === 'allow') return false;
if (isDraft) return /\sdata-quoted-html=|
`. The HTML tokenizer * ends the script at the first ``, say) used to close * the editor's script early and run as markup. `<` is written as `<`; * U+2028/2029 are escaped too, for engines that read them as line ends. */ export function jsonForScript(value: unknown): string { return JSON.stringify(value) .replace(//g, '>'); } export function buildEditorHtml(opts: { initialHtml: string; placeholder: string; c: ThemePalette; /** Block remote images and fonts (see `buildEditorCsp`). */ blockRemoteImages?: boolean; /** * Keep the page's visual viewport at the top and report every pan to RN as * a `viewportShift` (CSS px = dp) instead (see the page script). Android * only: its WebView pans the visual viewport to reveal the caret. */ pinVisualViewport?: boolean; }): string { const { initialHtml, placeholder, c, blockRemoteImages = false, pinVisualViewport = false } = 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, // and no ` `; }