194 lines
4.6 KiB
TypeScript
194 lines
4.6 KiB
TypeScript
import type { ListItem, PhrasingContent, Root, RootContent } from "mdast";
|
|
import { CodeBlockNodeName } from "prosemirror-codemirror-block";
|
|
import { Node } from "prosemirror-model";
|
|
import remarkStringify from "remark-stringify";
|
|
|
|
import { unifiedPipeline } from "..";
|
|
import { UNICODE_EMOJI_PACK_PUA } from "../emoji/UnicodeEmoji";
|
|
|
|
import { schema } from "./schema";
|
|
|
|
const pipeline = unifiedPipeline.use(remarkStringify);
|
|
|
|
// NB. https://github.com/syntax-tree/mdast
|
|
|
|
function map(node: Node): RootContent {
|
|
// apply marks
|
|
if (node.marks.length) {
|
|
const mark = node.marks[0];
|
|
switch (mark.type.name as keyof (typeof schema)["marks"]) {
|
|
case "strong":
|
|
return {
|
|
type: "strong",
|
|
children: [
|
|
map({
|
|
...node,
|
|
marks: node.marks.slice(1),
|
|
} as never) as PhrasingContent,
|
|
],
|
|
};
|
|
case "em":
|
|
return {
|
|
type: "emphasis",
|
|
children: [
|
|
map({
|
|
...node,
|
|
marks: node.marks.slice(1),
|
|
} as never) as PhrasingContent,
|
|
],
|
|
};
|
|
// todo: enable the gfm delete plugin ONLY
|
|
// case "strikethrough":
|
|
// return {
|
|
// type: "delete",
|
|
// children: [
|
|
// map({
|
|
// ...node,
|
|
// marks: node.marks.slice(1),
|
|
// } as never) as PhrasingContent,
|
|
// ],
|
|
// };
|
|
case "link":
|
|
return {
|
|
type: "link",
|
|
url: node.attrs.href,
|
|
title: node.attrs.title ?? node.attrs.href,
|
|
children: [
|
|
map({
|
|
...node,
|
|
marks: node.marks.slice(1),
|
|
} as never) as PhrasingContent,
|
|
],
|
|
};
|
|
case "code":
|
|
return {
|
|
type: "inlineCode",
|
|
value: node.text ?? "",
|
|
};
|
|
}
|
|
}
|
|
|
|
// apply node
|
|
switch (node.type.name as keyof (typeof schema)["nodes"]) {
|
|
case "paragraph":
|
|
return {
|
|
type: "paragraph",
|
|
children: node.children.map(map) as PhrasingContent[],
|
|
};
|
|
case "text":
|
|
return {
|
|
type: "text",
|
|
value: node.text!,
|
|
};
|
|
case CodeBlockNodeName:
|
|
return {
|
|
type: "code",
|
|
lang: node.attrs.lang,
|
|
value: node.textContent,
|
|
};
|
|
case "heading":
|
|
return {
|
|
type: "heading",
|
|
depth: node.attrs.level,
|
|
children: node.children.map(map) as PhrasingContent[],
|
|
};
|
|
case "bullet_list":
|
|
return {
|
|
type: "list",
|
|
ordered: false,
|
|
children: node.children.map(map) as ListItem[],
|
|
};
|
|
case "ordered_list":
|
|
return {
|
|
type: "list",
|
|
ordered: true,
|
|
start: node.attrs.order,
|
|
children: node.children.map(map) as ListItem[],
|
|
};
|
|
case "list_item":
|
|
return {
|
|
type: "listItem",
|
|
children: node.children.map(map) as never,
|
|
};
|
|
|
|
// RFM
|
|
|
|
case "rfm_custom_emoji":
|
|
return {
|
|
type: "text",
|
|
value: `:${node.attrs.id}:`,
|
|
};
|
|
case "rfm_unicode_emoji":
|
|
return {
|
|
type: "text",
|
|
value: `${UNICODE_EMOJI_PACK_PUA[node.attrs.pack as never] ?? ""}${node.attrs.id}`,
|
|
};
|
|
case "rfm_user_mention":
|
|
return {
|
|
type: "text",
|
|
value: `<@${node.attrs.id}>`,
|
|
};
|
|
case "rfm_role_mention":
|
|
return {
|
|
type: "text",
|
|
value: `<%${node.attrs.id}>`,
|
|
};
|
|
case "rfm_channel_mention":
|
|
return {
|
|
type: "text",
|
|
value: `<#${node.attrs.id}>`,
|
|
};
|
|
|
|
default:
|
|
console.info("Failing node:", node);
|
|
return {
|
|
type: "text",
|
|
value: `[missing ${node.type.name} serializer]`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Regex for matching double new lines
|
|
*/
|
|
const RE_DOUBLE_NEW_LINES = /\n\n/g;
|
|
|
|
/**
|
|
* Regex for matching special marker in codeblock
|
|
*/
|
|
const RE_CODEBLOCK_MARKER = /(?<=`{3}[\s\S]*)\uF8FF(?=[\s\S]*`{3})/gm;
|
|
|
|
/**
|
|
* Regex for matching special marker
|
|
*/
|
|
const RE_MARKER = /\uF8FF/g;
|
|
|
|
export function markdownFromProseMirrorModel(model: Node) {
|
|
// console.info(model);
|
|
|
|
if (model.type.name !== "doc") {
|
|
throw "root node should be 'doc'?";
|
|
}
|
|
|
|
const root: Root = {
|
|
type: "root",
|
|
children: model.children.map(map),
|
|
};
|
|
|
|
// console.info(JSON.stringify(root));
|
|
// console.info(pipeline.stringify(root));
|
|
|
|
return (
|
|
pipeline
|
|
.stringify(root)
|
|
// Replace double newlines with special marker
|
|
.replace(RE_DOUBLE_NEW_LINES, "\uF8FF")
|
|
// Skip the ones in codeblocks
|
|
.replace(RE_CODEBLOCK_MARKER, "\n\n")
|
|
// And now put a single newline
|
|
.replace(RE_MARKER, "\n")
|
|
// Strip spaces and newlines at start and end
|
|
.trim()
|
|
);
|
|
}
|