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