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]
50macro_rules! register {
51 ( $($rest:tt)* ) => {
52 $crate::__register_accum! { hook = (), storage = (), command = (), ui = (); $($rest)* }
53 };
54}
55
56#[doc(hidden)]
58#[macro_export]
59macro_rules! __register_accum {
60 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?); before_save: $f:path $(, $($rest:tt)*)? ) => {
61 $crate::__register_accum! { hook = ($f), storage = ($($s)?), command = ($($c)?), ui = ($($u)?); $($($rest)*)? }
62 };
63 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?); storage: $t:ty $(, $($rest:tt)*)? ) => {
64 $crate::__register_accum! { hook = ($($h)?), storage = ($t), command = ($($c)?), ui = ($($u)?); $($($rest)*)? }
65 };
66 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?); command: $f:path $(, $($rest:tt)*)? ) => {
67 $crate::__register_accum! { hook = ($($h)?), storage = ($($s)?), command = ($f), ui = ($($u)?); $($($rest)*)? }
68 };
69 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?); ui: $f:path $(, $($rest:tt)*)? ) => {
70 $crate::__register_accum! { hook = ($($h)?), storage = ($($s)?), command = ($($c)?), ui = ($f); $($($rest)*)? }
71 };
72 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?), ui = ($($u:path)?); ) => {
73 $crate::__register_dispatch! { hook = ($($h)?), storage = ($($s)?), command = ($($c)?), ui = ($($u)?) }
74 };
75}
76
77#[doc(hidden)]
78#[macro_export]
79macro_rules! __register_dispatch {
80 ( hook = ($($hook:path)?), storage = ($($st:ty)?), command = ($($cmd:path)?), ui = ($($ui:path)?) ) => {
81 #[allow(dead_code)]
83 fn __jasper_dispatch(
84 method: &str,
85 params: $crate::serde_json::Value,
86 ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> {
87 $(
88 if let Some(r) = $crate::storage::dispatch_storage::<$st>(method, ¶ms) {
89 return r;
90 }
91 )?
92 match method {
93 "metadata" => Ok($crate::serde_json::json!({ "ok": true })),
94 $(
95 "hook.before_save" => {
96 let note: $crate::core::model::Note = $crate::serde_json::from_value(
97 params.get("note").cloned().unwrap_or($crate::serde_json::Value::Null),
98 )
99 .map_err(|e| $crate::PluginError::invalid(format!("note 解析失败: {e}")))?;
100 let hook: fn(
101 $crate::core::model::Note,
102 ) -> ::std::result::Result<$crate::core::model::Note, String> = $hook;
103 let out = hook(note).map_err($crate::PluginError::internal)?;
104 Ok($crate::serde_json::json!({ "note": out }))
105 }
106 )?
107 $(
108 "command" => {
109 let id = params
110 .get("id")
111 .and_then(|v| v.as_str())
112 .unwrap_or("")
113 .to_string();
114 let args = params.get("args").cloned().unwrap_or($crate::serde_json::Value::Null);
115 let run: fn(
116 &str,
117 $crate::serde_json::Value,
118 ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> = $cmd;
119 run(&id, args)
120 }
121 )?
122 $(
123 "ui" => {
124 let view = params
125 .get("view")
126 .and_then(|v| v.as_str())
127 .unwrap_or("")
128 .to_string();
129 let state = params.get("state").cloned().unwrap_or($crate::serde_json::Value::Null);
130 let render: fn(
131 &str,
132 $crate::serde_json::Value,
133 ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> = $ui;
134 render(&view, state)
135 }
136 )?
137 other => Err($crate::PluginError::unsupported(format!("未知方法: {other}"))),
138 }
139 }
140
141 #[cfg(target_arch = "wasm32")]
142 mod __jasper_plugin_abi {
143 #[no_mangle]
144 pub extern "C" fn plugin_alloc(size: u32) -> u32 {
145 $crate::rt::alloc(size as usize) as u32
146 }
147 #[no_mangle]
148 pub extern "C" fn plugin_free(ptr: u32, size: u32) {
149 $crate::rt::free(ptr as usize, size as usize)
150 }
151 #[no_mangle]
152 pub extern "C" fn plugin_dispatch(ptr: u32, len: u32) -> u64 {
153 $crate::rt::dispatch(ptr as usize, len as usize, super::__jasper_dispatch)
154 }
155 }
156 };
157}
158
159#[cfg(test)]
160mod tests {
161 use crate::core::model::{MarkupLanguage, Note};
162 use serde_json::json;
163
164 fn trim_hook(mut note: Note) -> Result<Note, String> {
165 note.body = note.body.lines().map(|l| l.trim_end()).collect::<Vec<_>>().join("\n");
166 Ok(note)
167 }
168
169 crate::register! { before_save: trim_hook }
170
171 fn sample_note() -> Note {
172 Note {
173 id: "a".repeat(32),
174 parent_id: String::new(),
175 title: "t".into(),
176 body: "x \ny\t".into(),
177 created_time: 0,
178 updated_time: 0,
179 markup_language: MarkupLanguage::Markdown,
180 is_todo: false,
181 todo_completed: false,
182 is_conflict: false,
183 source_url: String::new(),
184 order: 0,
185 }
186 }
187
188 #[test]
189 fn dispatch_metadata_and_hook() {
190 let r = __jasper_dispatch("metadata", json!(null)).unwrap();
191 assert_eq!(r, json!({ "ok": true }));
192
193 let r = __jasper_dispatch("hook.before_save", json!({ "note": sample_note() })).unwrap();
194 assert_eq!(r["note"]["body"], "x\ny");
195
196 let e = __jasper_dispatch("nope", json!(null)).unwrap_err();
197 assert_eq!(e.code, "unsupported");
198 }
199}
200
201#[cfg(test)]
202mod ui_slot_tests {
203 use crate::PluginError;
204 use serde_json::{json, Value};
205
206 fn run(id: &str, args: Value) -> Result<Value, PluginError> {
207 Ok(json!({ "echoed": { "id": id, "args": args } }))
208 }
209
210 fn render(view: &str, state: Value) -> Result<Value, PluginError> {
211 Ok(json!({
212 "type": "markdown",
213 "props": { "source": format!("view={view}") },
214 "children": [ { "type": "button", "props": { "label": "go", "command": "x", "state": state } } ],
215 }))
216 }
217
218 crate::register! { command: run, ui: render }
219
220 #[test]
221 fn dispatch_routes_ui_and_command() {
222 let r = __jasper_dispatch("ui", json!({ "view": "main", "state": { "n": 1 } })).unwrap();
223 assert_eq!(r["type"], "markdown");
224 assert_eq!(r["props"]["source"], "view=main");
225 assert_eq!(r["children"][0]["props"]["state"]["n"], 1);
226
227 let r = __jasper_dispatch("command", json!({ "id": "c1", "args": { "a": 2 } })).unwrap();
228 assert_eq!(r["echoed"]["id"], "c1");
229
230 let e = __jasper_dispatch("hook.before_save", json!({ "note": null })).unwrap_err();
232 assert_eq!(e.code, "unsupported");
233 }
234}