/**
* 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))),
);
}