Files
multica/apps/mobile/lib/markdown/preprocess.ts
Naiyuan Qing 0d1601be8b chore(mobile): markdown rendering + UI primitive polish
- lib/markdown/{code-block,markdown-style,preprocess}: refined code block rendering, restructured style map, preprocess tweaks
- components/ui/{actor-avatar,text-field}: visual polish
- components/issue/mention-suggestion-bar: tweaks alongside inline composer mention pipeline
- components/editor/use-file-attach: small adjustments

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:52:06 +08:00

95 lines
4.1 KiB
TypeScript

/**
* 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 <br> / <sub> / <details>) — RN has
* no inline HTML. Without this pass, users see literal `<br>` tags in the
* comment body. Strategy:
*
* - `<br>` / `<br/>` / `<br />` → `" \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 `<br>` semantics.
* - HTML comments `<!-- ... -->` → removed entirely.
* - Every other tag → strip the tag, keep the inner text. So
* `<sub>2</sub>` 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(/<!--[\s\S]*?-->/g, "")
.replace(/<br\s*\/?>/gi, " \n")
.replace(/<\/?[a-z][^>]*>/gi, "");
}
export function preprocessMobileMarkdown(input: string): string {
if (!input) return "";
return preprocessTaskListStrikethrough(
preprocessFileCards(preprocessMentionShortcodes(stripHtml(input))),
);
}