11console.log('🌊')
2+
23import { Crepe } from "@milkdown/crepe";
34import "@milkdown/crepe/theme/common/style.css";
45import "@milkdown/crepe/theme/frame.css";
6+ import { upload, uploadConfig } from "@milkdown/kit/plugin/upload";
7+ import type { Uploader } from "@milkdown/kit/plugin/upload";
8+ import type { Node } from "@milkdown/kit/prose/model";
9+ // Function to convert a blob URL to a file
10+ async function blobUrlToFile(blobUrl: string): Promise<File> {
11+ try {
12+ const response = await fetch(blobUrl);
13+ if (!response.ok) {
14+ throw new Error(`Failed to fetch blob: ${response.status} ${response.statusText}`);
15+ }
16+ const blob = await response.blob();
17+ const filename = `image-${Date.now()}`;
18+ return new File([blob], filename, { type: blob.type });
19+ } catch (error) {
20+ console.error("Error converting blob URL to file:", error);
21+ throw error;
22+ }
23+ }
24+ // Function to convert a base64 image to a file
25+ async function base64ToFile(base64String: string): Promise<File> {
26+ try {
27+ // Extract MIME type and base64 data
28+ const matches = base64String.match(/^data:(image\/\w+);base64,(.+)$/);
29+ if (!matches || matches.length !== 3) {
30+ throw new Error("Invalid base64 image format");
31+ }
32+ const mimeType = matches[1];
33+ const base64Data = matches[2];
34+ // Convert base64 to blob
35+ const byteCharacters = atob(base64Data);
36+ const byteArrays = [];
37+ for (let i = 0; i < byteCharacters.length; i++) {
38+ byteArrays.push(byteCharacters.charCodeAt(i));
39+ }
40+ const byteArray = new Uint8Array(byteArrays);
41+ const blob = new Blob([byteArray], {type: mimeType});
42+ // Create a file
43+ const filename = `image-${Date.now()}`;
44+ return new File([blob], filename, { type: mimeType });
45+ } catch (error) {
46+ console.error("Error converting base64 to file:", error);
47+ throw error;
48+ }
49+ }
550
6- document.addEventListener("DOMContentLoaded", () => {
7- const editorElement = document.querySelector("#editor");
8- const markdownContent = editorElement.getAttribute("data-markdown") || "";
51+ const uploadImageToServer = async (file: File): Promise<string> => {
52+ console.log("Uploading image to server:", file.name);
53+
54+ // Retrieve the CSRF token from the hidden input
55+ const csrfTokenElement = document.querySelector('input[name="#csrf_token"]') as HTMLInputElement;
56+ if (!csrfTokenElement) {
57+ throw new Error("CSRF token not found in the page");
58+ }
59+ const csrfToken = csrfTokenElement.value;
960
10- const crepe = new Crepe({
11- root: "#editor",
12- defaultValue: markdownContent,
13- });
61+ // Create the FormData and add the file and CSRF token
62+ const formData = new FormData();
63+ formData.append("file", file);
64+ formData.append("#csrf_token", csrfToken); // Key corresponding to Session::CSRF_TOKEN_KEY
1465
15- crepe.create().then(() => {
16- console.log("Milkdown editor initialised with existing Markdown content");
66+ try {
67+ const response = await fetch("/admin/upload-image", {
68+ method: "POST",
69+ body: formData,
70+ // credentials: 'include', // Include cookies for CSRF token
71+ });
72+ if (!response.ok) {
73+ const errorData = await response.json().catch(() => ({}));
74+ throw new Error(`Upload failed: ${response.status} - ${JSON.stringify(errorData)}`);
75+ }
76+ const result = await response.json();
77+ if (!result.url) {
78+ throw new Error("No URL returned by the server");
79+ }
80+ return result.url;
81+ } catch (error) {
82+ console.error("Error while uploading the image:", error);
83+ throw error;
84+ }
85+ };
1786
18- // Update the textarea before submitting the form
19- const form = document.querySelector("#postForm");
20- form.addEventListener("submit", (event) => {
21- const markdownInput = document.querySelector("#markdownInput");
22- markdownInput.value = crepe.getMarkdown(); // Retrieve modified Markdown content
23- console.log(
24- "Markdown content updated in textarea before form submission"
25- );
26- // Optionally, you can prevent the form submission for debugging purposes
27- // event.preventDefault();
28- // console.log("Form submission prevented for debugging");
29- // console.log("Markdown content:", crepe.getMarkdown());
87+ const customUploader: Uploader = async (files, schema) => {
88+ console.log("Files received for upload:", files);
89+ const images: File[] = [];
90+ for (let i = 0; i < files.length; i++) {
91+ const file = files.item(i);
92+ if (file && file.type.includes("image")) {
93+ images.push(file);
94+ }
95+ }
96+ const nodes = await Promise.all(
97+ images.map(async (image) => {
98+ try {
99+ const src = await uploadImageToServer(image);
100+ const alt = image.name;
101+ console.log("Image uploaded:", src);
102+ return schema.nodes.image.createAndFill({ src, alt }) as Node;
103+ } catch (error) {
104+ console.error("Error uploading image:", error);
105+ return schema.nodes.image.createAndFill({
106+ src: "",
107+ alt: "Upload failed"
108+ }) as Node;
109+ }
110+ })
111+ );
112+ return nodes;
113+ };
114+
115+ document.addEventListener("DOMContentLoaded", () => {
116+ const editorElement = document.querySelector("#editor");
117+ if (!editorElement) {
118+ console.error("The #editor element is not found");
119+ return;
120+ }
121+ const markdownContent = editorElement.getAttribute("data-markdown") || "";
122+ const crepe = new Crepe({
123+ root: "#editor",
124+ defaultValue: markdownContent,
30125 });
31- });
32- });
126+ crepe.create()
127+ .then(() => {
128+ console.log("Editor instance created successfully");
129+ // Configure upload plugin
130+ crepe.editor.config((ctx) => {
131+ ctx.update(uploadConfig.key, (prev) => ({
132+ ...prev,
133+ uploader: customUploader,
134+ }));
135+ });
136+ crepe.editor.use(upload);
137+ console.log("Milkdown editor initialized with upload plugin");
138+ const form = document.querySelector("#postForm");
139+ if (form) {
140+ form.addEventListener("submit", async (event) => {
141+ event.preventDefault();
142+ console.log("Form submission intercepted");
143+ const markdownInput = document.querySelector("#markdownInput") as HTMLTextAreaElement;
144+ if (!markdownInput) {
145+ console.error("The #markdownInput element is not found");
146+ return;
147+ }
148+ let markdownContent = crepe.getMarkdown();
149+ console.log("Original markdown content:", markdownContent);
150+ // Regular expression to find markdown images with blob URLs or base64
151+ const imageUrlRegex = /!\[.*?\]\((blob:[^)]+|data:image\/\w+;base64,[^)]+)\)/g;
152+ const imageMatches = [...markdownContent.matchAll(imageUrlRegex)];
153+ if (imageMatches.length === 0) {
154+ console.log("No embedded images found, submitting form directly");
155+ markdownInput.value = markdownContent;
156+ form.submit();
157+ return;
158+ }
159+ console.log(`Found ${imageMatches.length} embedded images to process`);
160+ try {
161+ // Create a copy of the markdown content to modify
162+ let updatedMarkdown = markdownContent;
163+ // Map to store already processed URLs (to avoid duplicates)
164+ const processedUrls = new Map<string, string>();
165+ for (const match of imageMatches) {
166+ const fullMatch = match[0];
167+ const imageUrl = match[1];
168+ // If we've already processed this URL, replace directly
169+ if (processedUrls.has(imageUrl)) {
170+ const serverUrl = processedUrls.get(imageUrl);
171+ updatedMarkdown = updatedMarkdown.replace(fullMatch, ``);
172+ continue;
173+ }
174+ try {
175+ let serverImageUrl;
176+ let file;
177+ if (imageUrl.startsWith('blob:')) {
178+ console.log(`Processing blob URL: ${imageUrl}`);
179+ file = await blobUrlToFile(imageUrl);
180+ } else if (imageUrl.startsWith('data:image/')) {
181+ console.log(`Processing base64 image`);
182+ file = await base64ToFile(imageUrl);
183+ } else {
184+ // Not a blob or base64, skip
185+ continue;
186+ }
187+ // Upload the file to the server
188+ serverImageUrl = await uploadImageToServer(file);
189+ // Store the processed URL for subsequent occurrences
190+ processedUrls.set(imageUrl, serverImageUrl);
191+ // Replace the URL in the markdown
192+ updatedMarkdown = updatedMarkdown.replace(fullMatch, ``);
193+ console.log(`Replaced embedded image with server URL: ${serverImageUrl}`);
194+ } catch (error) {
195+ console.error(`Error processing image ${imageUrl}:`, error);
196+ // Continue with other images even if one fails
197+ }
198+ }
199+ // Update the markdown content in the hidden field
200+ markdownInput.value = updatedMarkdown;
201+ console.log("Updated markdown content with server image URLs:", updatedMarkdown);
202+ // Submit the form
203+ form.submit();
204+ } catch (error) {
205+ console.error("Error processing images before form submission:", error);
206+ alert("An error occurred while processing images. Please try again.");
207+ }
208+ });
209+ } else {
210+ console.error("The #postForm is not found");
211+ }
212+ })
213+ .catch((error) => {
214+ console.error("Error initializing the editor:", error);
215+ if (document.querySelector("#editor")) {
216+ document.querySelector("#editor").innerHTML =
217+ `<div class="error">The editor could not be loaded. Error: ${error.message}</div>`;
218+ }
219+ });
220+ });
0 commit comments