import { Fragment, type ReactNode } from 'react'; import { Platform, type ViewStyle } from 'react-native'; import { Renderer, useMarkdown, type MarkedStyles } from 'react-native-marked'; // Shared markdown rendering for text and paper particles. Mirrors the desktop // Crepe palette (markdown-editor.css `--crepe-*`) so a message reads the same // on both surfaces: white-on-transparent text, a blue accent, pink inline // code, and a near-opaque dark surface behind code blocks and tables. // // Known gap vs desktop: fenced code blocks aren't syntax-highlighted (Crepe // uses CodeMirror; react-native-marked only exposes the language tag). They // render as plain monospace on the dark surface, which is acceptable for v1. const TEXT_COLOR = 'rgba(255,255,255,0.92)'; const ACCENT = '#60a5fa'; const SURFACE = 'rgba(24,24,28,0.96)'; const OUTLINE = 'rgba(255,255,255,0.2)'; const MONO = Platform.OS === 'ios' ? 'Menlo' : 'monospace'; // react-native-marked doesn't render GFM task-list checkboxes (marked strips // the `[ ]`/`[x]` into token flags the parser ignores), so a write/read drift // shows up as bullets with no box. Swap the marker for a checkbox glyph before // parsing — read-only, matching desktop's bullet-free checkboxes. const TASK_ITEM_RE = /^(\s*)[-*+] \[([ xX])\] /gm; function withTaskCheckboxes(markdown: string): string { return markdown.replace( TASK_ITEM_RE, (_match, indent: string, mark: string) => `${indent}${mark === ' ' ? '☐' : '☑'} `, ); } const MARKDOWN_THEME = { colors: { text: TEXT_COLOR, link: ACCENT, code: SURFACE, border: OUTLINE, }, }; const MARKDOWN_STYLES: MarkedStyles = { text: { color: TEXT_COLOR, fontSize: 18, lineHeight: 28 }, li: { color: TEXT_COLOR, fontSize: 18, lineHeight: 28 }, strong: { fontWeight: '700' }, em: { fontStyle: 'italic' }, strikethrough: { textDecorationLine: 'line-through', color: 'rgba(255,255,255,0.6)', }, // fontStyle "normal" cancels react-native-marked's italic-by-default for // links and inline code (desktop renders neither italic). link: { color: ACCENT, fontStyle: 'normal' }, // borderBottomWidth 0 removes the library's default heading underline rule, // which desktop's headings don't have. h1: { color: '#ffffff', fontSize: 28, lineHeight: 34, fontWeight: '700', marginTop: 8, marginBottom: 8, borderBottomWidth: 0, }, h2: { color: '#ffffff', fontSize: 24, lineHeight: 30, fontWeight: '700', marginTop: 8, marginBottom: 6, borderBottomWidth: 0, }, h3: { color: '#ffffff', fontSize: 20, lineHeight: 26, fontWeight: '600', marginTop: 6, marginBottom: 4, }, h4: { color: '#ffffff', fontSize: 18, lineHeight: 24, fontWeight: '600', marginTop: 6, marginBottom: 4, }, h5: { color: '#ffffff', fontSize: 16, lineHeight: 22, fontWeight: '600', marginTop: 4, marginBottom: 2, }, h6: { color: 'rgba(255,255,255,0.7)', fontSize: 15, lineHeight: 20, fontWeight: '600', marginTop: 4, marginBottom: 2, }, codespan: { color: '#fca5a5', fontFamily: MONO, fontStyle: 'normal', backgroundColor: 'rgba(255,255,255,0.1)', }, code: { backgroundColor: SURFACE, borderColor: OUTLINE, borderWidth: 1, borderRadius: 8, padding: 12, marginVertical: 6, }, blockquote: { borderLeftWidth: 3, borderLeftColor: OUTLINE, paddingLeft: 12, marginVertical: 6, opacity: 0.85, }, // hr is left to the library default, which already draws a 1px rule in the // themed border color (OUTLINE). table: { borderWidth: 1, borderColor: OUTLINE, marginVertical: 6 }, tableRow: { borderColor: OUTLINE }, tableCell: { borderColor: OUTLINE, padding: 8 }, }; // react-native-marked feeds fenced code blocks the `em` (italic, proportional) // text style, so out of the box code renders italic in the body font. Override // `code` to apply a monospace, non-italic style instead — matching desktop's // code blocks. const CODE_TEXT_STYLE = { color: TEXT_COLOR, fontFamily: MONO, fontSize: 15, lineHeight: 22, }; class MarkdownRenderer extends Renderer { code(text: string, language?: string, containerStyle?: ViewStyle): ReactNode { return super.code(text, language, containerStyle, CODE_TEXT_STYLE); } } const MARKDOWN_RENDERER = new MarkdownRenderer(); /** * Renders GFM markdown using the shared Flowy palette. `useMarkdown` returns an * array of block nodes; we splat them into a Fragment so they nest cleanly * inside a parent ScrollView (vs. the library's own FlatList-based component). */ export function MarkdownBody({ content }: { content: string }) { const nodes = useMarkdown(withTaskCheckboxes(content), { renderer: MARKDOWN_RENDERER, theme: MARKDOWN_THEME, styles: MARKDOWN_STYLES, }); return {nodes}; }