/** * Pure string transforms applied before marked.lexer parses the content. * * Two passes, both idempotent: * 1. Legacy mention shortcodes `[@ id="..." label="..."]` → modern * mention link `[@Label](mention://member/id)`. Old DB rows from before * the April 2026 migration use the shortcode form; the modern form is * what marked.js can naturally tokenize as a markdown link. Calls into * `@multica/core/markdown` (single source of truth — same regex web/ * desktop run). * * 2. File card lines `!file[name](url)` → standard link `[📎 name](url)`. * marked.js doesn't recognize the `!file` prefix; web's preprocess * turns it into HTML, which mobile can't render natively. Rewriting * to a normal link with a 📎 emoji makes it a tappable link that * `Linking.openURL` opens in the system viewer (Safari for PDFs, * QuickLook for docs, share sheet for arbitrary files). * * NOTE: Web's preprocess also has a third pass that detects bare CDN * URLs as legacy file links. We skip that because mobile doesn't bootstrap * the cdnDomain config. Old comments using the legacy form render as plain * hyperlinks — same tap behavior, just no 📎 prefix. Acceptable degradation. */ import { preprocessMentionShortcodes } from "@multica/core/markdown"; const FILE_LINE_RE = /^!file\[([^\]]+)\]\((https?:\/\/[^)]+)\)$/; function preprocessFileCards(input: string): string { return input .split("\n") .map((line) => { const m = line.trim().match(FILE_LINE_RE); if (!m) return line; return `[📎 ${m[1]}](${m[2]})`; }) .join("\n"); } /** * Add GFM strikethrough markers around the content of checked task list items * so they render with `~~text~~` styling — matching Linear / Notion / Apple * Reminders / Things 3, where a checked item is visually crossed out. * * GFM itself does not specify that checked items SHOULD be struck through; * enriched-markdown's task-list renderer only changes the checkbox glyph and * (via `checkedTextColor`) dims the text. Without the strikethrough the * "done" state reads weakly, and users who expect the platform pattern from * other task apps assume the checkbox didn't take effect. * * Idempotent: skips lines whose body is already wrapped in `~~ ... ~~`. * Conservative regex — only matches `- [x]` / `* [x]` / `+ [x]` at the start * of a line (allowing leading whitespace), case-insensitive on the `x`. */ const TASK_DONE_RE = /^(\s*[-*+]\s+\[[xX]\]\s+)(.+)$/gm; function preprocessTaskListStrikethrough(input: string): string { return input.replace(TASK_DONE_RE, (match, prefix, body) => { const trimmed = body.trim(); if (trimmed.startsWith("~~") && trimmed.endsWith("~~")) return match; return `${prefix}~~${body}~~`; }); } /** * Strip embedded HTML before marked sees it. Mobile cannot do what web does * (rehype-raw + sanitize → render real
/ /
) — RN has * no inline HTML. Without this pass, users see literal `
` tags in the * comment body. Strategy: * * - `
` / `
` / `
` → `" \n"` (two trailing spaces + newline, * the canonical CommonMark hard-break syntax). md4c respects it as a * hard line break inside a paragraph; bare `\n` would be treated as a * space (CommonMark default), losing intentional `
` semantics. * - HTML comments `` → removed entirely. * - Every other tag → strip the tag, keep the inner text. So * `2` becomes `2`. Loses formatting but keeps content; far * better than showing raw HTML. * * Does not parse — pure regex. Cannot handle nested tags with attributes * containing `>`, but those don't appear in our editor output. */ function stripHtml(input: string): string { return input .replace(//g, "") .replace(//gi, " \n") .replace(/<\/?[a-z][^>]*>/gi, ""); } export function preprocessMobileMarkdown(input: string): string { if (!input) return ""; return preprocessTaskListStrikethrough( preprocessFileCards(preprocessMentionShortcodes(stripHtml(input))), ); }