Skip to main content

aidoku/imports/
js.rs

1//! Module for running JavaScript and managing web views.
2use super::{
3	FFIResult, Rid,
4	net::Request,
5	std::{destroy, read_string_and_destroy},
6};
7use crate::alloc::String;
8
9#[link(wasm_import_module = "js")]
10unsafe extern "C" {
11	fn context_create() -> Rid;
12	fn context_eval(context: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
13	fn context_eval_async(context: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
14	fn context_get(context: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
15
16	fn webview_create() -> Rid;
17	fn webview_set_rule_list(webview: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
18	fn webview_load(webview: Rid, request: Rid) -> FFIResult;
19	fn webview_load_html(
20		webview: Rid,
21		string_ptr: *const u8,
22		len: usize,
23		url_ptr: *const u8,
24		url_len: usize,
25	) -> FFIResult;
26	fn webview_wait_for_load(webview: Rid) -> FFIResult;
27	fn webview_eval(webview: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
28	fn webview_eval_async(webview: Rid, string_ptr: *const u8, len: usize) -> FFIResult;
29	fn webview_add_user_script(
30		webview: Rid,
31		string_ptr: *const u8,
32		len: usize,
33		at_document_end: bool,
34		for_main_frame_only: bool,
35	) -> FFIResult;
36}
37
38/// Error type for JavaScript operations.
39#[derive(PartialEq, Eq, Debug, Clone)]
40pub enum JsError {
41	MissingResult,
42	InvalidContext,
43	InvalidString,
44	InvalidHandler,
45	InvalidRequest,
46	InvalidRuleList,
47}
48
49impl JsError {
50	fn from(value: FFIResult) -> Option<Self> {
51		match value {
52			-1 => Some(Self::MissingResult),
53			-2 => Some(Self::InvalidContext),
54			-3 => Some(Self::InvalidString),
55			-4 => Some(Self::InvalidHandler),
56			-5 => Some(Self::InvalidRequest),
57			-6 => Some(Self::InvalidRuleList),
58			_ => None,
59		}
60	}
61}
62
63/// A context for evaluating JavaScript code.
64pub struct JsContext {
65	rid: Rid,
66}
67
68impl JsContext {
69	/// Creates a new JavaScript context.
70	pub fn new() -> Self {
71		let rid = unsafe { context_create() };
72		Self { rid }
73	}
74
75	/// Evaluates JavaScript code in the context.
76	pub fn eval(&self, js: &str) -> Result<String, JsError> {
77		let js_bytes = js.as_bytes();
78		let result = unsafe { context_eval(self.rid, js_bytes.as_ptr(), js_bytes.len()) };
79		if let Some(error) = JsError::from(result) {
80			Err(error)
81		} else {
82			Ok(read_string_and_destroy(result).unwrap_or_default())
83		}
84	}
85
86	/// Evaluates asynchronous JavaScript code in the context.
87	pub fn eval_async(&self, js: &str) -> Result<String, JsError> {
88		let js_bytes = js.as_bytes();
89		let result = unsafe { context_eval_async(self.rid, js_bytes.as_ptr(), js_bytes.len()) };
90		if let Some(error) = JsError::from(result) {
91			Err(error)
92		} else {
93			Ok(read_string_and_destroy(result).unwrap_or_default())
94		}
95	}
96
97	/// Retrieves the value of a JavaScript variable in the context.
98	pub fn get(&self, variable: &str) -> Result<String, JsError> {
99		let var_bytes = variable.as_bytes();
100		let result = unsafe { context_get(self.rid, var_bytes.as_ptr(), var_bytes.len()) };
101		if let Some(error) = JsError::from(result) {
102			Err(error)
103		} else {
104			Ok(read_string_and_destroy(result).unwrap_or_default())
105		}
106	}
107}
108
109impl Default for JsContext {
110	fn default() -> Self {
111		Self::new()
112	}
113}
114
115impl Drop for JsContext {
116	fn drop(&mut self) {
117		unsafe { destroy(self.rid) }
118	}
119}
120
121/// A web view that can be used to load web content.
122///
123/// This web view won't be displayed to the user. It is intended for use in the background.
124pub struct WebView {
125	rid: Rid,
126}
127
128impl WebView {
129	/// Creates a new web view.
130	pub fn new() -> Self {
131		let rid = unsafe { webview_create() };
132		Self { rid }
133	}
134
135	/// Sets a content rule list for the web view.
136	///
137	/// For information on formatting the rule list json, see Apple's documentation on
138	/// [Creating a content blocker](https://developer.apple.com/documentation/SafariServices/creating-a-content-blocker).
139	pub fn set_rule_list(&self, json: &str) -> Result<(), JsError> {
140		let json_bytes = json.as_bytes();
141		let result =
142			unsafe { webview_set_rule_list(self.rid, json_bytes.as_ptr(), json_bytes.len()) };
143		if let Some(error) = JsError::from(result) {
144			Err(error)
145		} else {
146			Ok(())
147		}
148	}
149
150	/// Loads a web page in the web view.
151	pub fn load(&self, request: Request) -> Result<(), JsError> {
152		let request_descriptor = request.rid;
153		let result = unsafe { webview_load(self.rid, request_descriptor) };
154		if let Some(error) = JsError::from(result) {
155			Err(error)
156		} else {
157			Ok(())
158		}
159	}
160
161	/// Loads a web page in the web view, blocking until the page is loaded.
162	pub fn load_blocking(&self, request: Request) -> Result<(), JsError> {
163		self.load(request)?;
164		self.wait_for_load();
165		Ok(())
166	}
167
168	/// Loads the given HTML content in the web view.
169	pub fn load_html(&self, html: &str, base_url: Option<&str>) -> Result<(), JsError> {
170		let html_bytes = html.as_bytes();
171		let url_bytes = base_url.map(|s| s.as_bytes()).unwrap_or_default();
172		let result = unsafe {
173			webview_load_html(
174				self.rid,
175				html_bytes.as_ptr(),
176				html_bytes.len(),
177				url_bytes.as_ptr(),
178				url_bytes.len(),
179			)
180		};
181		if let Some(error) = JsError::from(result) {
182			Err(error)
183		} else {
184			Ok(())
185		}
186	}
187
188	/// Loads HTML content in the web view, blocking until the content is loaded.
189	pub fn load_html_blocking(&self, html: &str, base_url: Option<&str>) -> Result<(), JsError> {
190		self.load_html(html, base_url)?;
191		self.wait_for_load();
192		Ok(())
193	}
194
195	/// Blocks the current thread until the web view is loaded.
196	pub fn wait_for_load(&self) {
197		unsafe { webview_wait_for_load(self.rid) };
198	}
199
200	/// Evaluates JavaScript code in the web view, blocking until the result is available.
201	pub fn eval(&self, js: &str) -> Result<String, JsError> {
202		let js_bytes = js.as_bytes();
203		let result = unsafe { webview_eval(self.rid, js_bytes.as_ptr(), js_bytes.len()) };
204		if let Some(error) = JsError::from(result) {
205			Err(error)
206		} else {
207			Ok(read_string_and_destroy(result).unwrap_or_default())
208		}
209	}
210
211	/// Evaluates asynchronous JavaScript code in the web view, blocking until the result is available.
212	pub fn eval_async(&self, js: &str) -> Result<String, JsError> {
213		let js_bytes = js.as_bytes();
214		let result = unsafe { webview_eval_async(self.rid, js_bytes.as_ptr(), js_bytes.len()) };
215		if let Some(error) = JsError::from(result) {
216			Err(error)
217		} else {
218			Ok(read_string_and_destroy(result).unwrap_or_default())
219		}
220	}
221
222	/// Adds a user script to the web view.
223	pub fn add_user_script(&self, script: WebViewUserScript) -> Result<(), JsError> {
224		let source_bytes = script.source.as_bytes();
225		let result = unsafe {
226			webview_add_user_script(
227				self.rid,
228				source_bytes.as_ptr(),
229				source_bytes.len(),
230				script.at_document_end,
231				script.for_main_frame_only,
232			)
233		};
234		if let Some(error) = JsError::from(result) {
235			Err(error)
236		} else {
237			Ok(())
238		}
239	}
240}
241
242impl Default for WebView {
243	fn default() -> Self {
244		Self::new()
245	}
246}
247
248impl Drop for WebView {
249	fn drop(&mut self) {
250		unsafe { destroy(self.rid) }
251	}
252}
253
254#[derive(Default)]
255/// An object that represents a script that can be injected into webpages
256pub struct WebViewUserScript {
257	/// The script source.
258	pub source: String,
259	/// Whether script should be injected at the end of a document or the start.
260	pub at_document_end: bool,
261	/// Whether the script should be injected into all frames or just the main frame.
262	pub for_main_frame_only: bool,
263}
264
265impl WebViewUserScript {
266	/// Creates a new user script.
267	pub fn new(source: String) -> Self {
268		Self {
269			source,
270			..Default::default()
271		}
272	}
273}