stoat-for-desktop/components/markdown/prosemirror/to-model.ts

187 lines
4.7 KiB
TypeScript

import { Dayjs } from "dayjs";
import { Root, RootContent } from "mdast";
import { Mark, Node } from "prosemirror-model";
import { Client } from "stoat.js";
import { UNIFIED_PLUGINS, unifiedPipeline } from "..";
import { UnicodeEmojiPacks, unicodeEmojiUrl } from "../emoji/UnicodeEmoji";
import { schema } from "./schema";
export function blankModel() {
return schema.nodes.doc.createChecked(
null,
schema.nodes.paragraph.createChecked(null),
);
}
interface Context {
parent?: RootContent["type"];
marks?: Mark[];
}
type RfmComponents =
| {
type: "mention";
mentions: string;
}
| {
type: "customEmoji";
id: string;
}
| {
type: "unicodeEmoji";
str: string;
pack?: UnicodeEmojiPacks;
}
| {
type: "timestamp";
format: string;
date: Dayjs;
};
function map(
node: RootContent | RfmComponents,
client: Client,
context: Context = {},
): Node[] | Node {
switch (node.type) {
case "paragraph":
return schema.nodes.paragraph.createChecked(
null,
node.children.flatMap((child) =>
map(child, client, { parent: "paragraph" }),
),
);
case "text": {
if (!node.value) return [];
return schema.text(node.value, context.marks);
}
case "strong":
return node.children.flatMap((child) =>
map(child, client, {
marks: [...(context.marks ?? []), schema.marks.strong.create()],
}),
);
case "emphasis":
return node.children.flatMap((child) =>
map(child, client, {
marks: [...(context.marks ?? []), schema.marks.em.create()],
}),
);
case "link":
return node.children.flatMap((child) =>
map(child, client, {
marks: [
...(context.marks ?? []),
schema.marks.link.create({
href: node.url,
}),
],
}),
);
case "delete":
return node.children.flatMap((child) =>
map(child, client, {
marks: [
...(context.marks ?? []),
schema.marks.strikethrough.create(),
],
}),
);
case "heading":
return schema.nodes.heading.createChecked(
{ level: node.depth },
node.children.flatMap((child) =>
map(child, client, { parent: "heading" }),
),
);
case "list":
if (node.ordered) {
return schema.nodes.ordered_list.createChecked(
{
start: node.start,
},
node.children.flatMap((child) =>
map(child, client, { parent: "list" }),
),
);
} else {
return schema.nodes.bullet_list.createChecked(
null,
node.children.flatMap((child) =>
map(child, client, { parent: "list" }),
),
);
}
case "listItem":
return schema.nodes.list_item.createChecked(
null,
node.children.flatMap((child) =>
map(child, client, { parent: "listItem" }),
),
);
case "inlineCode":
return schema.text(node.value, [
...(context.marks ?? []),
schema.marks.code.create(),
]);
// RFM
case "mention":
if (node.mentions.startsWith("user:")) {
const id = node.mentions.substring(5);
const user = client.users.get(id);
if (user) {
return schema.nodes.rfm_user_mention.createAndFill({
id,
username: user.username,
avatar: user.animatedAvatarURL,
})!;
} else {
return schema.text(`<@${id}>`);
}
} else {
return schema.text("no");
}
case "customEmoji":
return schema.nodes.rfm_custom_emoji.createAndFill({
id: node.id,
src: `https://cdn.revoltusercontent.com/emojis/${node.id}`,
})!;
case "unicodeEmoji":
return schema.nodes.rfm_unicode_emoji.createAndFill({
id: node.str,
pack: node.pack,
src: unicodeEmojiUrl(node.pack, node.str),
})!;
case "timestamp":
return schema.text(`<t:${node.date.unix()}:${node.format}>`);
default: {
console.info("Failing node:", node);
const text = schema.text(`[missing ${node.type} serializer]`);
if (context.parent === "paragraph") return [text];
return [schema.nodes.paragraph.createChecked(null, text)];
}
}
}
export function markdownToProseMirrorModel(content: string, client: Client) {
if (!content) return blankModel();
const tree = unifiedPipeline.parse(content);
for (const plugin of UNIFIED_PLUGINS) {
(plugin as never as () => (tree: Root) => void)()(tree);
}
// console.info(tree);
return schema.nodes.doc.createChecked(
null,
tree.children.flatMap((child) => map(child, client)),
);
}