Files
multica/apps/mobile/lib/markdown/split-markdown.ts
Jiayuan Zhangandmultica-agent 231dedb552 MUL-8034: chore: clean historical comments across the codebase (#9274)
* chore: clean historical comments (MUL-8034)

Co-authored-by: multica-agent <[email protected]>

* docs: preserve pending Composio expiry contract

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-10-15 21:54:33 +08:00

92 lines
2.6 KiB
TypeScript

/**
* Split Markdown into native prose, React code blocks, and interactive images.
* Use lexer token boundaries so fenced/indented code and paragraph images are
* handled without fragile regex reconstruction. Preserve prose through token.raw.
*
* Only top-level code is extracted. Nested list/quote code stays in the native
* renderer to preserve its surrounding Markdown structure.
*/
import { marked, type Tokens } from "marked";
export type MarkdownSegment =
| { type: "prose"; content: string }
| { type: "code"; lang: string | undefined; code: string }
| { type: "image"; uri: string; alt: string };
export function splitMarkdown(input: string): MarkdownSegment[] {
if (!input) return [];
const tokens = marked.lexer(input);
const out: MarkdownSegment[] = [];
let proseBuffer = "";
const flushProse = () => {
const trimmed = proseBuffer.replace(/^\s+|\s+$/g, "");
if (trimmed.length > 0) {
out.push({ type: "prose", content: trimmed });
}
proseBuffer = "";
};
for (const token of tokens) {
if (token.type === "code") {
flushProse();
const t = token as Tokens.Code;
out.push({
type: "code",
lang: t.lang ? t.lang : undefined,
code: t.text,
});
continue;
}
if (token.type === "paragraph") {
const para = token as Tokens.Paragraph;
const inline = para.tokens ?? [];
const hasImage = inline.some((t) => t.type === "image");
if (!hasImage) {
proseBuffer += para.raw;
continue;
}
// Split paragraph at each image. Text fragments before/after each
// image rejoin the prose buffer as their own short paragraph.
let textBuffer = "";
const flushText = () => {
const trimmed = textBuffer.trim();
if (trimmed.length > 0) {
// Append as its own paragraph so enriched treats consecutive
// fragments as separate blocks rather than running them together.
proseBuffer += trimmed + "\n\n";
}
textBuffer = "";
};
for (const t of inline) {
if (t.type === "image") {
flushText();
flushProse();
const img = t as Tokens.Image;
out.push({
type: "image",
uri: img.href,
alt: img.text ?? "",
});
} else {
textBuffer += (t as { raw?: string }).raw ?? "";
}
}
flushText();
continue;
}
// Every other top-level block (heading, list, blockquote, table,
// hr, html, space, def, …) goes to prose verbatim via token.raw.
proseBuffer += (token as { raw?: string }).raw ?? "";
}
flushProse();
return out;
}