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