mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 19:06:06 +02:00
- 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>
95 lines
4.1 KiB
TypeScript
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))),
|
|
);
|
|
}
|