1pub mod host;
23#[cfg(all(not(target_arch = "wasm32"), feature = "native-host"))]
24pub mod native_host;
25pub mod rt;
26pub mod storage;
27
28pub use jasper_core as core;
29pub use rt::PluginError;
30pub use serde_json;
32
33#[cfg(target_arch = "wasm32")]
36mod rand_shim {
37 fn no_entropy(_buf: &mut [u8]) -> Result<(), getrandom::Error> {
38 Err(getrandom::Error::UNSUPPORTED)
39 }
40 getrandom::register_custom_getrandom!(no_entropy);
41}
42
43#[macro_export]
52macro_rules! register {
53 ( $($rest:tt)* ) => {
54 $crate::__register_accum! { hook = (), storage = (), command = (), ui = (), editor = (); $($rest)* }
55 };
56}
57
58#[doc(hidden)]
60#[macro_export]
61macro_rules! __register_accum {
62 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?), editor = ($($e:path)?); before_save: $f:path $(, $($rest:tt)*)? ) => {
63 $crate::__register_accum! { hook = ($f), storage = ($($s)?), command = ($($c)?), ui = ($($u)?), editor = ($($e)?); $($($rest)*)? }
64 };
65 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?), editor = ($($e:path)?); storage: $t:ty $(, $($rest:tt)*)? ) => {
66 $crate::__register_accum! { hook = ($($h)?), storage = ($t), command = ($($c)?), ui = ($($u)?), editor = ($($e)?); $($($rest)*)? }
67 };
68 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?), editor = ($($e:path)?); command: $f:path $(, $($rest:tt)*)? ) => {
69 $crate::__register_accum! { hook = ($($h)?), storage = ($($s)?), command = ($f), ui = ($($u)?), editor = ($($e)?); $($($rest)*)? }
70 };
71 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?), editor = ($($e:path)?); ui: $f:path $(, $($rest:tt)*)? ) => {
72 $crate::__register_accum! { hook = ($($h)?), storage = ($($s)?), command = ($($c)?), ui = ($f), editor = ($($e)?); $($($rest)*)? }
73 };
74 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?), editor = ($($e:path)?); editor: $f:path $(, $($rest:tt)*)? ) => {
75 $crate::__register_accum! { hook = ($($h)?), storage = ($($s)?), command = ($($c)?), ui = ($($u)?), editor = ($f); $($($rest)*)? }
76 };
77 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?), editor = ($($e:path)?); ) => {
78 $crate::__register_dispatch! { hook = ($($h)?), storage = ($($s)?), command = ($($c)?), ui = ($($u)?), editor = ($($e)?) }
79 };
80}
81
82#[doc(hidden)]
83#[macro_export]
84macro_rules! __register_dispatch {
85 ( hook = ($($hook:path)?), storage = ($($st:ty)?), command = ($($cmd:path)?), ui = ($($ui:path)?), editor = ($($editor:path)?) ) => {
86 #[allow(dead_code)]
88 fn __jasper_dispatch(
89 method: &str,
90 params: $crate::serde_json::Value,
91 ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> {
92 $(
93 if let Some(r) = $crate::storage::dispatch_storage::<$st>(method, ¶ms) {
94 return r;
95 }
96 )?
97 match method {
98 "metadata" => Ok($crate::serde_json::json!({ "ok": true })),
99 $(
100 "hook.before_save" => {
101 let note: $crate::core::model::Note = $crate::serde_json::from_value(
102 params.get("note").cloned().unwrap_or($crate::serde_json::Value::Null),
103 )
104 .map_err(|e| $crate::PluginError::invalid(format!("note 解析失败: {e}")))?;
105 let hook: fn(
106 $crate::core::model::Note,
107 ) -> ::std::result::Result<$crate::core::model::Note, String> = $hook;
108 let out = hook(note).map_err($crate::PluginError::internal)?;
109 Ok($crate::serde_json::json!({ "note": out }))
110 }
111 )?
112 $(
113 "command" => {
114 let id = params
115 .get("id")
116 .and_then(|v| v.as_str())
117 .unwrap_or("")
118 .to_string();
119 let args = params.get("args").cloned().unwrap_or($crate::serde_json::Value::Null);
120 let run: fn(
121 &str,
122 $crate::serde_json::Value,
123 ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> = $cmd;
124 run(&id, args)
125 }
126 )?
127 $(
128 "ui" => {
129 let view = params
130 .get("view")
131 .and_then(|v| v.as_str())
132 .unwrap_or("")
133 .to_string();
134 let state = params.get("state").cloned().unwrap_or($crate::serde_json::Value::Null);
135 let render: fn(
136 &str,
137 $crate::serde_json::Value,
138 ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> = $ui;
139 render(&view, state)
140 }
141 )?
142 $(
143 "editor.transform" => {
144 let phase = params
145 .get("phase")
146 .and_then(|v| v.as_str())
147 .unwrap_or("")
148 .to_string();
149 let text = params
150 .get("text")
151 .and_then(|v| v.as_str())
152 .unwrap_or("")
153 .to_string();
154 let transform: fn(
155 &str,
156 String,
157 ) -> ::std::result::Result<String, $crate::PluginError> = $editor;
158 let out = transform(&phase, text)?;
159 Ok($crate::serde_json::json!({ "text": out }))
160 }
161 )?
162 other => Err($crate::PluginError::unsupported(format!("未知方法: {other}"))),
163 }
164 }
165
166 #[cfg(target_arch = "wasm32")]
167 mod __jasper_plugin_abi {
168 #[no_mangle]
169 pub extern "C" fn plugin_alloc(size: u32) -> u32 {
170 $crate::rt::alloc(size as usize) as u32
171 }
172 #[no_mangle]
173 pub extern "C" fn plugin_free(ptr: u32, size: u32) {
174 $crate::rt::free(ptr as usize, size as usize)
175 }
176 #[no_mangle]
177 pub extern "C" fn plugin_dispatch(ptr: u32, len: u32) -> u64 {
178 $crate::rt::dispatch(ptr as usize, len as usize, super::__jasper_dispatch)
179 }
180 }
181 };
182}
183
184#[cfg(test)]
185mod tests {
186 use crate::core::model::{MarkupLanguage, Note};
187 use serde_json::json;
188
189 fn trim_hook(mut note: Note) -> Result<Note, String> {
190 note.body = note.body.lines().map(|l| l.trim_end()).collect::<Vec<_>>().join("\n");
191 Ok(note)
192 }
193
194 crate::register! { before_save: trim_hook }
195
196 fn sample_note() -> Note {
197 Note {
198 id: "a".repeat(32),
199 parent_id: String::new(),
200 title: "t".into(),
201 body: "x \ny\t".into(),
202 created_time: 0,
203 updated_time: 0,
204 markup_language: MarkupLanguage::Markdown,
205 is_todo: false,
206 todo_completed: false,
207 is_conflict: false,
208 source_url: String::new(),
209 order: 0,
210 }
211 }
212
213 #[test]
214 fn dispatch_metadata_and_hook() {
215 let r = __jasper_dispatch("metadata", json!(null)).unwrap();
216 assert_eq!(r, json!({ "ok": true }));
217
218 let r = __jasper_dispatch("hook.before_save", json!({ "note": sample_note() })).unwrap();
219 assert_eq!(r["note"]["body"], "x\ny");
220
221 let e = __jasper_dispatch("nope", json!(null)).unwrap_err();
222 assert_eq!(e.code, "unsupported");
223 }
224}
225
226#[cfg(test)]
227mod ui_slot_tests {
228 use crate::PluginError;
229 use serde_json::{json, Value};
230
231 fn run(id: &str, args: Value) -> Result<Value, PluginError> {
232 Ok(json!({ "echoed": { "id": id, "args": args } }))
233 }
234
235 fn render(view: &str, state: Value) -> Result<Value, PluginError> {
236 Ok(json!({
237 "type": "markdown",
238 "props": { "source": format!("view={view}") },
239 "children": [ { "type": "button", "props": { "label": "go", "command": "x", "state": state } } ],
240 }))
241 }
242
243 crate::register! { command: run, ui: render }
244
245 #[test]
246 fn dispatch_routes_ui_and_command() {
247 let r = __jasper_dispatch("ui", json!({ "view": "main", "state": { "n": 1 } })).unwrap();
248 assert_eq!(r["type"], "markdown");
249 assert_eq!(r["props"]["source"], "view=main");
250 assert_eq!(r["children"][0]["props"]["state"]["n"], 1);
251
252 let r = __jasper_dispatch("command", json!({ "id": "c1", "args": { "a": 2 } })).unwrap();
253 assert_eq!(r["echoed"]["id"], "c1");
254
255 let e = __jasper_dispatch("hook.before_save", json!({ "note": null })).unwrap_err();
257 assert_eq!(e.code, "unsupported");
258 }
259}
260
261#[cfg(test)]
262mod editor_slot_tests {
263 use crate::PluginError;
264 use serde_json::json;
265
266 fn transform(phase: &str, text: String) -> Result<String, PluginError> {
268 Ok(format!("[{phase}] {}", text.to_uppercase()))
269 }
270
271 crate::register! { editor: transform }
272
273 #[test]
274 fn dispatch_routes_editor_transform() {
275 let r = __jasper_dispatch("editor.transform", json!({ "phase": "input", "text": "hi there" })).unwrap();
276 assert_eq!(r["text"], "[input] HI THERE");
277
278 let e = __jasper_dispatch("command", json!({ "id": "x" })).unwrap_err();
280 assert_eq!(e.code, "unsupported");
281 }
282}