返回 presentation-ai
markdown-infographic-rules.ts
根目录 / src / components / plate / plugins / markdown-infographic-rules.ts
1 import {
2 defaultRules,
3 type MdRules,
4 type SerializeMdOptions,
5 } from "@platejs/markdown";
6 import { nanoid, type TText } from "platejs";
7
8 import { ANTV_INFOGRAPHIC } from "@/components/notebook/presentation/editor/lib";
9 import { type TAntvInfographicElement } from "@/components/notebook/presentation/editor/plugins/antv-infographic-plugin";
10
11 /** Minimal mdast Code node type for deserialization */
12 interface MdastCodeNode {
13 type: "code";
14 lang?: string | null;
15 value: string;
16 position?: {
17 start: {
18 line: number;
19 column: number;
20 offset: number;
21 };
22 end: {
23 line: number;
24 column: number;
25 offset: number;
26 };
27 };
28 }
29
30 function getCodeBlockLanguage(line: string): string | null {
31 const trimmedLine = line.trimStart();
32
33 if (!trimmedLine.startsWith("```")) {
34 return null;
35 }
36
37 const [language] = trimmedLine.slice(3).trim().split(/\s+/, 1);
38 const normalizedLanguage = language?.trim().toLowerCase();
39
40 return normalizedLanguage ? normalizedLanguage : null;
41 }
42
43 function parseFencedCodeBlockValue(value: string): {
44 content: string;
45 language: string | null;
46 } {
47 const trimmedValue = value.trimStart();
48
49 if (!trimmedValue.startsWith("```")) {
50 return { content: value, language: null };
51 }
52
53 const lineBreakIndex = trimmedValue.indexOf("\n");
54 const openingLine =
55 lineBreakIndex === -1
56 ? trimmedValue
57 : trimmedValue.slice(0, lineBreakIndex);
58 const language = getCodeBlockLanguage(openingLine);
59
60 if (!language) {
61 return { content: value, language: null };
62 }
63
64 let content =
65 lineBreakIndex === -1 ? "" : trimmedValue.slice(lineBreakIndex + 1);
66 const closingFenceIndex = content.lastIndexOf("```");
67
68 if (closingFenceIndex >= 0) {
69 const trailingValue = content.slice(closingFenceIndex);
70
71 if (trailingValue.trim() === "```") {
72 content = content.slice(0, closingFenceIndex).trimEnd();
73 }
74 }
75
76 return { content, language };
77 }
78
79 function buildInfographicElement(
80 prompt: string,
81 id: string,
82 ): TAntvInfographicElement {
83 return {
84 type: ANTV_INFOGRAPHIC,
85 id,
86 generationPrompt: prompt.trim(),
87 isLoading: true,
88 syntax: "",
89 children: [{ text: "" } as TText],
90 };
91 }
92
93 function getInfographicId(mdastNode: MdastCodeNode): string {
94 if (mdastNode.position) {
95 return `i-${mdastNode.position.start.line}-${mdastNode.position.end.line}`;
96 }
97
98 return `i-${nanoid()}`;
99 }
100
101 function resolveCodeBlockNode(mdastNode: MdastCodeNode): MdastCodeNode {
102 const { content, language } = parseFencedCodeBlockValue(mdastNode.value);
103
104 if (!language) {
105 return mdastNode;
106 }
107
108 return {
109 ...mdastNode,
110 lang: language,
111 value: content,
112 };
113 }
114
115 type CodeBlockDeserializer = NonNullable<
116 NonNullable<MdRules["code_block"]>["deserialize"]
117 >;
118 type CodeBlockSerializer = NonNullable<
119 NonNullable<MdRules["code_block"]>["serialize"]
120 >;
121
122 function deserializeInfographicCodeBlock(
123 mdastNode: MdastCodeNode,
124 deco: Readonly<Partial<Record<string, string | boolean>>>,
125 options: Parameters<CodeBlockDeserializer>[2],
126 ): TAntvInfographicElement | ReturnType<CodeBlockDeserializer> {
127 const resolvedNode = resolveCodeBlockNode(mdastNode);
128 const language = resolvedNode.lang?.trim().toLowerCase();
129
130 if (language === "infographic") {
131 return buildInfographicElement(
132 resolvedNode.value,
133 getInfographicId(mdastNode),
134 );
135 }
136
137 const defaultCodeBlockDeserializer = defaultRules.code_block?.deserialize as
138 | CodeBlockDeserializer
139 | undefined;
140
141 if (!defaultCodeBlockDeserializer) {
142 throw new Error(
143 "Default markdown code block deserializer is not available.",
144 );
145 }
146
147 return defaultCodeBlockDeserializer(
148 resolvedNode as Parameters<CodeBlockDeserializer>[0],
149 deco,
150 options,
151 );
152 }
153
154 function serializeInfographicElement(
155 slateNode: TAntvInfographicElement,
156 _options: SerializeMdOptions,
157 ): MdastCodeNode {
158 const content =
159 typeof slateNode.syntax === "string" && slateNode.syntax.trim().length > 0
160 ? slateNode.syntax.trim()
161 : (slateNode.generationPrompt?.trim() ?? "");
162
163 return {
164 type: "code",
165 lang: "infographic",
166 value: content.length > 0 ? content : "Generate an infographic",
167 };
168 }
169
170 function serializeCodeBlock(
171 slateNode: Parameters<CodeBlockSerializer>[0],
172 options: Parameters<CodeBlockSerializer>[1],
173 ): ReturnType<CodeBlockSerializer> {
174 const defaultCodeBlockSerializer = defaultRules.code_block?.serialize as
175 | CodeBlockSerializer
176 | undefined;
177
178 if (!defaultCodeBlockSerializer) {
179 throw new Error("Default markdown code block serializer is not available.");
180 }
181
182 return defaultCodeBlockSerializer(slateNode, options);
183 }
184
185 export const infographicMarkdownRules: MdRules = {
186 code_block: {
187 deserialize:
188 deserializeInfographicCodeBlock as unknown as CodeBlockDeserializer,
189 serialize: serializeCodeBlock,
190 },
191 [ANTV_INFOGRAPHIC]: {
192 serialize: serializeInfographicElement,
193 },
194 };
195
195 lines TYPESCRIPT