mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 18:06:45 +08:00
* 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]>
92 lines
2.6 KiB
TypeScript
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;
|
|
}
|