Skip to main content

yggdrasil/bridges/
tiptap.rs

1//! Tiptap 编辑器的 wasm-bindgen 绑定层。
2//!
3//! 封装与 `window.TiptapEditor`(IIFE 暴露的全局对象)的全部交互,
4//! 替代旧版 `js_sys::eval` 字符串拼贴 + window 全局变量通信。
5//!
6//! wasm-bindgen extern 与 `EditorHandle`、上传 closure 等**仅在 WASM 前端**编译
7//! (server 构建无 `window`);共享的纯数据类型 `UploadsInFlight`/`UploadErrorEntry`
8//! 在两端都编译,供 `write.rs` 在 rsx 中渲染上传状态。
9
10/// 当前编辑器内进行中的上传计数(来自 onUploadEvent 的 counts 快照)。
11#[derive(Clone, Copy, Default)]
12pub struct UploadsInFlight {
13    pub uploading: u32,
14    pub error: u32,
15}
16
17/// 顶部堆叠的上传失败提示条目。
18#[derive(Clone, PartialEq)]
19pub struct UploadErrorEntry {
20    pub id: String,
21    pub file_name: String,
22    pub message: String,
23}
24
25// ============================================================================
26// 以下全部仅在 WASM 前端编译:wasm-bindgen extern + EditorHandle + 上传 closure。
27// 放在 #[cfg] 子模块内,避免 server 构建尝试编译引用 JS 对象的 extern。
28// ============================================================================
29#[cfg(target_arch = "wasm32")]
30pub mod wasm {
31    use super::{UploadErrorEntry, UploadsInFlight};
32    // WritableExt 提供 .write()(Signal 在 Copy 语义下不需要 mut 绑定)。
33    use dioxus::prelude::WritableExt;
34    use wasm_bindgen::prelude::*;
35    use wasm_bindgen::JsCast;
36
37    // —— window.TiptapEditor 模块对象 ——
38    //
39    // TiptapEditor 是 IIFE 产物挂在 window 上的模块对象(含 create 等方法),
40    // 不是函数。wasm-bindgen 对 `fn get_module() -> T` 形式的 extern 会生成
41    // `window.TiptapEditor()`(函数调用),会因"not a function"失败。
42    // 因此用 js_sys::Reflect::get 做属性访问拿到模块对象,再 dyn_into。
43    #[wasm_bindgen]
44    extern "C" {
45        /// `window.TiptapEditor` 模块对象的 Rust 映射(IIFE 产物挂在 window 上的对象字面量)。
46        /// 不是函数——通过 [`get_module`] 用 Reflect::get 取属性而非 extern fn 调用拿到。
47        pub type TiptapEditorModule;
48
49        /// 调用 `TiptapEditor.create(containerId, options)`。
50        /// 找不到容器返回 null(被 Option 捕获);构造失败抛异常(被 catch 捕获)。
51        #[wasm_bindgen(method, catch)]
52        pub fn create(
53            this: &TiptapEditorModule,
54            container_id: &str,
55            opts: &EditorOptions,
56        ) -> Result<Option<EditorInstance>, JsValue>;
57    }
58
59    /// 读取 `window.TiptapEditor`(IIFE 默认导出,顶层 var 即 window 属性)。
60    /// 用 Reflect::get 做属性访问——extern fn 形式会被 wasm-bindgen 编成函数调用。
61    ///
62    /// 用 unchecked_into 而非 dyn_into:TiptapEditor 是 JS 对象字面量,
63    /// 不是 wasm-bindgen 注册的构造函数实例,dyn_into 的 instanceof 检查必然失败。
64    /// unchecked_into 只做编译期类型标注,不做运行时校验(Reflect.get 已保证拿到的是目标对象)。
65    pub fn get_module() -> TiptapEditorModule {
66        let window = web_sys::window().expect("no window");
67        let val = js_sys::Reflect::get(&window, &"TiptapEditor".into())
68            .expect("window.TiptapEditor missing");
69        val.unchecked_into::<TiptapEditorModule>()
70    }
71
72    // —— 编辑器实例(TiptapEditorInstance)——
73    #[wasm_bindgen]
74    extern "C" {
75        /// `TiptapEditor.create` 返回的编辑器实例对象,承载 ProseMirror 编辑器与上传协调器。
76        pub type EditorInstance;
77
78        /// 富文本模式下返回 ProseMirror 的 Markdown;源码模式下返回 textarea 内容。
79        #[wasm_bindgen(method, js_name = getMarkdown)]
80        pub fn get_markdown(this: &EditorInstance) -> String;
81
82        /// 用 Markdown 内容回填编辑器(emitUpdate: false,不触发 onUpdate)。
83        #[wasm_bindgen(method, js_name = setMarkdown)]
84        pub fn set_markdown(this: &EditorInstance, content: &str);
85
86        /// 按 uploadId 删除上传节点(revoke blob + 删 pending)。供宿主"×关闭"调用。
87        #[wasm_bindgen(method, js_name = removeUploadByUploadId)]
88        pub fn remove_upload_by_upload_id(this: &EditorInstance, upload_id: &str) -> bool;
89
90        /// 工具栏图片按钮入口:把文件交给 coordinator 走占位符上传
91        /// (与粘贴/拖放同一条路径)。
92        #[wasm_bindgen(method, js_name = insertUploading)]
93        pub fn insert_uploading(this: &EditorInstance, file: web_sys::File);
94
95        /// 从素材库批量插入图片:`json` 为 `[{ "src", "alt"? }]` 串
96        /// (`AssetSelection` 的 serde 序列化形状)。JS 侧在 slash 命令删除
97        /// `/素材` 文本后停留的光标位置一次事务插入全部图片块;非法载荷 no-op。
98        #[wasm_bindgen(method, js_name = insertImagesFromLibrary)]
99        pub fn insert_images_from_library(this: &EditorInstance, json: &str);
100
101        /// 销毁编辑器,释放 JS 侧资源。
102        #[wasm_bindgen(method)]
103        pub fn destroy(this: &EditorInstance);
104    }
105
106    // —— EditorOptions:用 builder 模式(setter)构造 JS 对象 ——
107    #[wasm_bindgen]
108    extern "C" {
109        /// 传给 `TiptapEditor.create` 的配置对象,对应 JS 侧的 EditorOptions。
110        /// 用 `new()` 创建空对象后通过 setter 链式设置字段。
111        pub type EditorOptions;
112
113        /// 构造一个空的 EditorOptions,随后用各 setter 填充回调与占位文案。
114        #[wasm_bindgen(constructor)]
115        pub fn new() -> EditorOptions;
116
117        /// 编辑器无内容时的占位文案。
118        #[wasm_bindgen(method, setter, js_name = placeholder)]
119        pub fn set_placeholder(this: &EditorOptions, v: &str);
120
121        /// 编辑器变体:`"full"`(默认,后台文章编辑器)或 `"comment"`
122        /// (评论区精简子集:气泡菜单 + Placeholder,无标题/表格/斜杠命令等)。
123        #[wasm_bindgen(method, setter, js_name = variant)]
124        pub fn set_variant(this: &EditorOptions, v: &str);
125
126        /// 文档变更回调(ProseMirror transaction 提交时触发,参数为最新 Markdown)。
127        #[wasm_bindgen(method, setter, js_name = onUpdate)]
128        pub fn set_on_update(this: &EditorOptions, cb: &Closure<dyn FnMut(String)>);
129
130        /// JS 侧 onImageUpload: (file: File) => Promise<string>。
131        /// Rust closure 返回 js_sys::Promise,由 future_to_promise 包装。
132        #[wasm_bindgen(method, setter, js_name = onImageUpload)]
133        pub fn set_on_image_upload(
134            this: &EditorOptions,
135            cb: &Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
136        );
137
138        /// 编辑器就绪回调(init 末尾同步触发一次)。
139        #[wasm_bindgen(method, setter, js_name = onReady)]
140        pub fn set_on_ready(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
141
142        /// 上传事件回调(coordinator.emit 时触发,携带 counts)。
143        #[wasm_bindgen(method, setter, js_name = onUploadEvent)]
144        pub fn set_on_upload_event(this: &EditorOptions, cb: &Closure<dyn FnMut(UploadEventJs)>);
145
146        /// JS 侧 onRunCode: (opts: RunCodeOptsJs) => Promise<string>。
147        /// Rust closure 返回 js_sys::Promise,内部调 start_exec + 轮询 get_exec_result。
148        #[wasm_bindgen(method, setter, js_name = onRunCode)]
149        pub fn set_on_run_code(
150            this: &EditorOptions,
151            cb: &Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
152        );
153
154        /// JS 侧 onPickFromLibrary: () => void。
155        /// slash 命令「素材库」触发(此时 /命令 文本已被 JS 删除、光标停在删除位置);
156        /// Rust 侧打开 AssetPickerModal,确认后由 insertImagesFromLibrary 回填。
157        #[wasm_bindgen(method, setter, js_name = onPickFromLibrary)]
158        pub fn set_on_pick_from_library(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
159    }
160
161    // —— 上传事件(JS UploadEvent 的 Rust 映射)——
162    #[wasm_bindgen]
163    extern "C" {
164        /// 上传协调器 emit 的事件对象,对应 JS 侧 UploadCoordinator 发出的事件。
165        /// 由 `onUploadEvent` 回调回传 Rust,[`consume_upload_event`] 据其更新 UI 状态。
166        #[derive(Clone)]
167        pub type UploadEventJs;
168
169        /// 事件种类:`"uploading"` / `"success"` / `"error"` / `"removed"`。
170        #[wasm_bindgen(method, getter)]
171        pub fn kind(this: &UploadEventJs) -> String;
172
173        /// 本次上传的唯一标识(前端生成,用于关联上传节点与失败提示条目)。
174        #[wasm_bindgen(method, getter, js_name = uploadId)]
175        pub fn upload_id(this: &UploadEventJs) -> String;
176
177        /// 上传文件名(用于失败提示展示)。
178        #[wasm_bindgen(method, getter, js_name = fileName)]
179        pub fn file_name(this: &UploadEventJs) -> String;
180
181        /// 失败原因(仅 `error` 事件有值,成功/移除事件为 None)。
182        #[wasm_bindgen(method, getter, js_name = errorMsg)]
183        pub fn error_msg(this: &UploadEventJs) -> Option<String>;
184
185        /// 当前文档内全部上传节点的实时计数快照。
186        #[wasm_bindgen(method, getter)]
187        pub fn counts(this: &UploadEventJs) -> UploadCountsJs;
188    }
189
190    #[wasm_bindgen]
191    extern "C" {
192        /// 上传计数快照(JS 侧遍历文档节点统计后随事件下发)。
193        #[derive(Clone)]
194        pub type UploadCountsJs;
195
196        /// 进行中的上传数量。
197        #[wasm_bindgen(method, getter)]
198        pub fn uploading(this: &UploadCountsJs) -> u32;
199
200        /// 失败的上传数量。
201        #[wasm_bindgen(method, getter)]
202        pub fn error(this: &UploadCountsJs) -> u32;
203    }
204
205    // —— RunCodeOptsJs:onRunCode 回调参数(JS 侧传给 Rust 的纯数据对象)——
206    /// JS 侧传给 onRunCode 的参数对象,Rust 侧读取 getter。
207    /// language 是纯语言名(如 "python",前端已 extractLang 提取);overridesJson 是 overrides 的 JSON 字符串(可能为空)。
208    #[wasm_bindgen]
209    extern "C" {
210        pub type RunCodeOptsJs;
211
212        /// 纯语言名(前端 extractLang 从完整 info string 提取,如 "python")。
213        #[wasm_bindgen(method, getter)]
214        pub fn language(this: &RunCodeOptsJs) -> String;
215
216        /// 代码块文本内容。
217        #[wasm_bindgen(method, getter)]
218        pub fn source(this: &RunCodeOptsJs) -> String;
219
220        /// overrides 的 JSON 字符串(如 `{"timeout_secs":10}`),空串表示无 overrides。
221        /// 前端从 info string 提取(大括号部分),Rust 用 serde_json 反序列化。
222        #[wasm_bindgen(method, getter, js_name = overridesJson)]
223        pub fn overrides_json(this: &RunCodeOptsJs) -> String;
224    }
225
226    // —— EditorHandle:实例 + closure 统一生命周期 ——
227
228    /// 持有编辑器实例 + 其全部 closure,统一生命周期。
229    ///
230    /// drop 时先 destroy JS 实例(释放 ProseMirror 资源),随后 closure 字段
231    /// 按声明逆序自动 drop(释放 wasm-bindgen 回调表)。比 `Closure::forget`
232    /// (永久泄漏)干净——closure 严格随编辑器实例同生共死。
233    pub struct EditorHandle {
234        instance: EditorInstance,
235        _on_update: Closure<dyn FnMut(String)>,
236        _on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
237        _on_ready: Closure<dyn FnMut()>,
238        _on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
239        _on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
240        _on_pick_from_library: Closure<dyn FnMut()>,
241    }
242
243    impl EditorHandle {
244        /// 聚合编辑器实例与回调 closure,使其共用同一生命周期。
245        ///
246        /// 调用方负责先用各 setter 把 closure 装进 [`EditorOptions`]、`create` 出实例后,
247        /// 再把实例与这些 closure 一并交由本函数持有。返回的 [`EditorHandle`] 一旦
248        /// drop,会先 `destroy` 实例、再按字段逆序 drop closure。
249        pub fn new(
250            instance: EditorInstance,
251            on_update: Closure<dyn FnMut(String)>,
252            on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
253            on_ready: Closure<dyn FnMut()>,
254            on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
255            on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
256            on_pick_from_library: Closure<dyn FnMut()>,
257        ) -> Self {
258            Self {
259                instance,
260                _on_update: on_update,
261                _on_image_upload: on_image_upload,
262                _on_ready: on_ready,
263                _on_upload_event: on_upload_event,
264                _on_run_code: on_run_code,
265                _on_pick_from_library: on_pick_from_library,
266            }
267        }
268
269        /// 访问底层编辑器实例(调 getMarkdown/setMarkdown/destroy 等)。
270        pub fn instance(&self) -> &EditorInstance {
271            &self.instance
272        }
273
274        /// 评论编辑器用构造:只传入 comment variant 实际使用的 4 个 closure。
275        ///
276        /// run_code / pick_from_library 在 comment variant 下没有触发方
277        /// (无斜杠命令、无代码块 NodeView),以 no-op closure 补齐生命周期结构。
278        pub fn new_comment(
279            instance: EditorInstance,
280            on_update: Closure<dyn FnMut(String)>,
281            on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
282            on_ready: Closure<dyn FnMut()>,
283            on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
284        ) -> Self {
285            Self::new(
286                instance,
287                on_update,
288                on_image_upload,
289                on_ready,
290                on_upload_event,
291                Closure::new(|_opts: RunCodeOptsJs| js_sys::Promise::resolve(&JsValue::null())),
292                Closure::new(|| {}),
293            )
294        }
295    }
296
297    impl Drop for EditorHandle {
298        fn drop(&mut self) {
299            self.instance.destroy();
300        }
301    }
302
303    // —— consume_upload_event:纯逻辑 helper ——
304
305    /// 消费单个上传事件,更新 signal(即时驱动,替代旧版 500ms 轮询)。
306    ///
307    /// 逻辑与旧轮询 body 一致:
308    /// - error:新 id 追加提示,已存在 id 原地更新消息(重试后再失败)
309    /// - success/removed:移除对应提示
310    /// - counts:直接从事件读(JS 已遍历文档算好)
311    ///
312    /// 去重以 `upload_errors` Vec 自身为唯一数据源(用 iter().any 判重),
313    /// 不再额外维护 seen_error_ids,避免两份状态需手动同步。
314    pub fn consume_upload_event(
315        ev: &UploadEventJs,
316        mut uploads_in_flight: dioxus::prelude::Signal<UploadsInFlight>,
317        mut upload_errors: dioxus::prelude::Signal<Vec<UploadErrorEntry>>,
318    ) {
319        let id = ev.upload_id();
320        match ev.kind().as_str() {
321            "error" => {
322                let msg = ev.error_msg().unwrap_or_else(|| "上传失败".to_string());
323                // 已存在同 id(重试后再失败):原地更新消息;否则追加。
324                let mut errors = upload_errors.write();
325                if let Some(entry) = errors.iter_mut().find(|e| e.id == id) {
326                    entry.message = msg;
327                } else {
328                    errors.push(UploadErrorEntry {
329                        id: id.clone(),
330                        file_name: ev.file_name(),
331                        message: msg,
332                    });
333                }
334            }
335            "success" | "removed" => {
336                upload_errors.write().retain(|e| e.id != id);
337            }
338            _ => {}
339        }
340        // counts 直接从事件读(JS 已算好)
341        let c = ev.counts();
342        uploads_in_flight.set(UploadsInFlight {
343            uploading: c.uploading(),
344            error: c.error(),
345        });
346    }
347
348    // —— make_upload_closure:Rust fetch 上传 ——
349
350    /// 通用图片上传:multipart POST 指定端点,解析 {success, url, error}。
351    ///
352    /// 由封面图上传(spawn 直接 await)与 Tiptap 编辑器上传(make_upload_closure 包 Promise)共用;
353    /// fetch + 契约解析样板收敛在 [`crate::utils::web_upload::post_multipart_file`]
354    /// (与备份导入共享),此处只取 `url` 字段。
355    ///
356    /// 行为:
357    /// - credentials: same-origin(携带 session cookie)
358    /// - 字段名 'image'(与服务端 upload.rs 对齐)
359    /// - 成功 → Ok(url);失败 → Err(服务端中文 error,或状态码兜底)
360    ///
361    /// 构造阶段的错误以 Err 返回,而非 panic,
362    /// 单张坏文件不应导致整个上传流程崩溃。
363    async fn upload_file_to(url: &str, file: web_sys::File) -> Result<String, String> {
364        let data = crate::utils::web_upload::post_multipart_file(url, "image", &file).await?;
365        let url = data["url"].as_str().unwrap_or("");
366        if url.is_empty() {
367            // success=true 但 url 为空:服务端契约异常,按失败处理
368            return Err("上传成功但未返回图片地址".to_string());
369        }
370        Ok(url.to_string())
371    }
372
373    /// admin 图片上传(POST /api/upload,需 admin 会话)。
374    pub async fn upload_image_file(file: web_sys::File) -> Result<String, String> {
375        upload_file_to("/api/upload", file).await
376    }
377
378    /// 评论图片上传(POST /api/comments/upload,允许匿名,IP 双层限流)。
379    pub async fn upload_comment_image_file(file: web_sys::File) -> Result<String, String> {
380        upload_file_to("/api/comments/upload", file).await
381    }
382
383    /// 创建 Tiptap 图片上传 closure:内部复用 [`upload_image_file`],包装成 JS Promise。
384    ///
385    /// 返回的 closure 签名 `(File) -> Promise` 对应 JS `onImageUpload`。
386    pub fn make_upload_closure() -> Closure<dyn Fn(web_sys::File) -> js_sys::Promise> {
387        Closure::new(move |file: web_sys::File| -> js_sys::Promise {
388            wasm_bindgen_futures::future_to_promise(async move {
389                upload_image_file(file)
390                    .await
391                    .map(|url| js_sys::JsString::from(url).into())
392                    .map_err(|msg| js_sys::Error::new(&msg).into())
393            })
394        })
395    }
396
397    /// 创建评论区 Tiptap 图片上传 closure:走匿名可用的评论端点
398    /// ([`upload_comment_image_file`]),其余语义与 [`make_upload_closure`] 一致。
399    pub fn make_comment_upload_closure() -> Closure<dyn Fn(web_sys::File) -> js_sys::Promise> {
400        Closure::new(move |file: web_sys::File| -> js_sys::Promise {
401            wasm_bindgen_futures::future_to_promise(async move {
402                upload_comment_image_file(file)
403                    .await
404                    .map(|url| js_sys::JsString::from(url).into())
405                    .map_err(|msg| js_sys::Error::new(&msg).into())
406            })
407        })
408    }
409
410    // —— make_run_code_closure:编辑器内运行代码 ——
411
412    /// 把 ExecTask 格式化为结果字符串(供编辑器结果区展示)。
413    fn format_run_result(task: &crate::api::code_runner::ExecTask) -> String {
414        use crate::api::code_runner::ExecStatus;
415        let status_label = match task.status {
416            ExecStatus::Success => "Success",
417            ExecStatus::Error => "Error",
418            ExecStatus::Timeout => "Timeout",
419            ExecStatus::OomKilled => "OOM",
420            ExecStatus::Failed => "Failed",
421            _ => "Unknown",
422        };
423        match &task.result {
424            Some(res) => {
425                let mut out = format!("状态: {} · 耗时: {}ms", status_label, res.duration_ms);
426                if !res.stdout.is_empty() {
427                    out.push_str("\nStdout:\n");
428                    out.push_str(&res.stdout);
429                }
430                if !res.stderr.is_empty() {
431                    out.push_str("\nStderr:\n");
432                    out.push_str(&res.stderr);
433                }
434                out
435            }
436            None => format!("状态: {} · {}", status_label, task.stage),
437        }
438    }
439
440    /// 创建「编辑器内运行代码」closure:内部调 start_exec + 轮询 get_exec_result,
441    /// 把格式化结果字符串回传 JS。
442    ///
443    /// 返回的 closure 签名 `(RunCodeOptsJs) -> Promise` 对应 JS `onRunCode`。
444    /// JS 侧 NodeView await Promise,拿到字符串直接填进结果区 DOM。
445    ///
446    /// 注意:info string 的解析(提取语言名 + overrides JSON)在前端 extractLang 完成,
447    /// Rust 收到的 language 已是纯语言名(如 "python"),不依赖 server-only 的 languages 模块。
448    pub fn make_run_code_closure() -> Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise> {
449        Closure::new(move |opts: RunCodeOptsJs| -> js_sys::Promise {
450            wasm_bindgen_futures::future_to_promise(async move {
451                use crate::api::code_runner::{execute, ExecRequest, ExecStatus};
452                use crate::infra::runner_config::ResourceLimits;
453
454                let language = opts.language();
455                let source = opts.source();
456                let overrides_json = opts.overrides_json();
457
458                // 反序列化 overrides JSON(前端已提取大括号部分;空串视为 None)
459                let overrides = if overrides_json.trim().is_empty() {
460                    None
461                } else {
462                    match serde_json::from_str::<ResourceLimits>(&overrides_json) {
463                        Ok(o) => Some(o),
464                        Err(_) => None, // 畸形 JSON 静默降级为无 overrides
465                    }
466                };
467
468                let req = ExecRequest {
469                    language,
470                    source,
471                    overrides,
472                };
473
474                match execute::start_exec(req).await {
475                    Ok(task_id) => {
476                        let poll_interval = 500;
477                        // 500ms * 60 = 30s 上限(编辑器内运行是写作辅助,比 reader 的 120s 短)
478                        for _ in 0..60 {
479                            crate::utils::time::sleep_ms(poll_interval).await;
480                            match execute::get_exec_result(task_id.clone()).await {
481                                Ok(task) => {
482                                    let terminal = task.status != ExecStatus::Queued
483                                        && task.status != ExecStatus::Running;
484                                    if terminal {
485                                        let s = format_run_result(&task);
486                                        return Ok(js_sys::JsString::from(s).into());
487                                    }
488                                }
489                                Err(_) => {
490                                    return Err(js_sys::Error::new("结果获取异常").into());
491                                }
492                            }
493                        }
494                        Err(js_sys::Error::new("轮询超时,请重试").into())
495                    }
496                    Err(e) => Err(js_sys::Error::new(&e.to_string()).into()),
497                }
498            })
499        })
500    }
501}
502
503/// 将 WASM 子模块中的桥接类型与函数重导出到 crate 根,供 `write.rs` 直接引用。
504/// server 构建剥离该子模块,故此重导出仅对 WASM 前端生效。
505#[cfg(target_arch = "wasm32")]
506pub use wasm::{
507    consume_upload_event, get_module, make_comment_upload_closure, make_run_code_closure,
508    make_upload_closure, upload_image_file, EditorHandle, EditorOptions, UploadEventJs,
509};