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]
49macro_rules! register {
50 ( $($rest:tt)* ) => {
51 $crate::__register_accum! { hook = (), storage = (), command = (); $($rest)* }
52 };
53}
54
55#[doc(hidden)]
57#[macro_export]
58macro_rules! __register_accum {
59 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?); before_save: $f:path $(, $($rest:tt)*)? ) => {
60 $crate::__register_accum! { hook = ($f), storage = ($($s)?), command = ($($c)?); $($($rest)*)? }
61 };
62 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?); storage: $t:ty $(, $($rest:tt)*)? ) => {
63 $crate::__register_accum! { hook = ($($h)?), storage = ($t), command = ($($c)?); $($($rest)*)? }
64 };
65 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?); command: $f:path $(, $($rest:tt)*)? ) => {
66 $crate::__register_accum! { hook = ($($h)?), storage = ($($s)?), command = ($f); $($($rest)*)? }
67 };
68 ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?); ) => {
69 $crate::__register_dispatch! { hook = ($($h)?), storage = ($($s)?), command = ($($c)?) }
70 };
71}
72
73#[doc(hidden)]
74#[macro_export]
75macro_rules! __register_dispatch {
76 ( hook = ($($hook:path)?), storage = ($($st:ty)?), command = ($($cmd:path)?) ) => {
77 #[allow(dead_code)]
79 fn __jasper_dispatch(
80 method: &str,
81 params: $crate::serde_json::Value,
82 ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> {
83 $(
84 if let Some(r) = $crate::storage::dispatch_storage::<$st>(method, ¶ms) {
85 return r;
86 }
87 )?
88 match method {
89 "metadata" => Ok($crate::serde_json::json!({ "ok": true })),
90 $(
91 "hook.before_save" => {
92 let note: $crate::core::model::Note = $crate::serde_json::from_value(
93 params.get("note").cloned().unwrap_or($crate::serde_json::Value::Null),
94 )
95 .map_err(|e| $crate::PluginError::invalid(format!("note 解析失败: {e}")))?;
96 let hook: fn(
97 $crate::core::model::Note,
98 ) -> ::std::result::Result<$crate::core::model::Note, String> = $hook;
99 let out = hook(note).map_err($crate::PluginError::internal)?;
100 Ok($crate::serde_json::json!({ "note": out }))
101 }
102 )?
103 $(
104 "command" => {
105 let id = params
106 .get("id")
107 .and_then(|v| v.as_str())
108 .unwrap_or("")
109 .to_string();
110 let args = params.get("args").cloned().unwrap_or($crate::serde_json::Value::Null);
111 let run: fn(
112 &str,
113 $crate::serde_json::Value,
114 ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> = $cmd;
115 run(&id, args)
116 }
117 )?
118 other => Err($crate::PluginError::unsupported(format!("未知方法: {other}"))),
119 }
120 }
121
122 #[cfg(target_arch = "wasm32")]
123 mod __jasper_plugin_abi {
124 #[no_mangle]
125 pub extern "C" fn plugin_alloc(size: u32) -> u32 {
126 $crate::rt::alloc(size as usize) as u32
127 }
128 #[no_mangle]
129 pub extern "C" fn plugin_free(ptr: u32, size: u32) {
130 $crate::rt::free(ptr as usize, size as usize)
131 }
132 #[no_mangle]
133 pub extern "C" fn plugin_dispatch(ptr: u32, len: u32) -> u64 {
134 $crate::rt::dispatch(ptr as usize, len as usize, super::__jasper_dispatch)
135 }
136 }
137 };
138}
139
140#[cfg(test)]
141mod tests {
142 use crate::core::model::{MarkupLanguage, Note};
143 use serde_json::json;
144
145 fn trim_hook(mut note: Note) -> Result<Note, String> {
146 note.body = note.body.lines().map(|l| l.trim_end()).collect::<Vec<_>>().join("\n");
147 Ok(note)
148 }
149
150 crate::register! { before_save: trim_hook }
151
152 fn sample_note() -> Note {
153 Note {
154 id: "a".repeat(32),
155 parent_id: String::new(),
156 title: "t".into(),
157 body: "x \ny\t".into(),
158 created_time: 0,
159 updated_time: 0,
160 markup_language: MarkupLanguage::Markdown,
161 is_todo: false,
162 todo_completed: false,
163 is_conflict: false,
164 source_url: String::new(),
165 order: 0,
166 }
167 }
168
169 #[test]
170 fn dispatch_metadata_and_hook() {
171 let r = __jasper_dispatch("metadata", json!(null)).unwrap();
172 assert_eq!(r, json!({ "ok": true }));
173
174 let r = __jasper_dispatch("hook.before_save", json!({ "note": sample_note() })).unwrap();
175 assert_eq!(r["note"]["body"], "x\ny");
176
177 let e = __jasper_dispatch("nope", json!(null)).unwrap_err();
178 assert_eq!(e.code, "unsupported");
179 }
180}