1#![allow(unsafe_code)]
21
22use std::cell::RefCell;
23use std::rc::Rc;
24use std::time::Instant;
25
26use boa_engine::{js_string, Context, JsValue, NativeFunction, Source};
27
28use crate::runtime::{ExecContext, ExecError, ExecOutput, ExitStatus, OutputFormat, Runtime};
29
30pub struct JsRuntime;
32
33const CONSOLE_SHIM: &str = r#"
34globalThis.console = {
35 log: (...a) => __outl_log(a.map(x => String(x)).join(' ') + '\n'),
36 warn: (...a) => __outl_log(a.map(x => String(x)).join(' ') + '\n'),
37 error: (...a) => __outl_log(a.map(x => String(x)).join(' ') + '\n'),
38 info: (...a) => __outl_log(a.map(x => String(x)).join(' ') + '\n'),
39};
40"#;
41
42impl Runtime for JsRuntime {
43 fn language(&self) -> &'static str {
44 "js"
45 }
46
47 fn execute(&self, source: &str, ctx: &ExecContext) -> Result<ExecOutput, ExecError> {
48 let start = Instant::now();
49 let mut context = Context::default();
50 let _ = ctx;
52 let sink: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
53
54 let log_sink = sink.clone();
58 let log_fn = unsafe {
59 NativeFunction::from_closure(move |_, args, ctx| {
60 if let Some(arg) = args.first() {
61 let s = arg.to_string(ctx)?;
62 log_sink.borrow_mut().push_str(&s.to_std_string_escaped());
63 }
64 Ok(JsValue::undefined())
65 })
66 };
67 context
68 .register_global_callable(js_string!("__outl_log"), 1, log_fn)
69 .map_err(|e| ExecError::Sandbox(format!("register __outl_log: {e}")))?;
70
71 #[cfg(feature = "lang-query")]
75 {
76 let ws_root = Rc::new(ctx.workspace_root.clone());
77 let query_fn = unsafe {
78 NativeFunction::from_closure(move |_, args, js_ctx| {
79 let root = ws_root.clone();
80 let arg0 = args.first().cloned().unwrap_or(JsValue::undefined());
81 let params = js_value_to_query_params(&arg0, js_ctx).map_err(|e| {
82 boa_engine::JsError::from(
83 boa_engine::error::JsNativeError::typ().with_message(e),
84 )
85 })?;
86 let hits = super::query::run_query_structured(¶ms, &root).map_err(|e| {
87 boa_engine::JsError::from(
88 boa_engine::error::JsNativeError::typ().with_message(e),
89 )
90 })?;
91 hits_to_js_array(&hits, js_ctx).map_err(|e| {
92 boa_engine::JsError::from(
93 boa_engine::error::JsNativeError::typ().with_message(e),
94 )
95 })
96 })
97 };
98 let outl_obj = boa_engine::object::ObjectInitializer::new(&mut context)
99 .function(query_fn, js_string!("query"), 1)
100 .build();
101 context
102 .register_global_property(
103 js_string!("outl"),
104 JsValue::from(outl_obj),
105 boa_engine::property::Attribute::all(),
106 )
107 .map_err(|e| ExecError::Sandbox(format!("register outl: {e}")))?;
108 }
109 let _ = context
112 .eval(Source::from_bytes(CONSOLE_SHIM))
113 .map_err(|e| ExecError::Sandbox(format!("console shim: {e}")))?;
114
115 let value = match context.eval(Source::from_bytes(source)) {
118 Ok(v) => v,
119 Err(e) => {
120 return Ok(ExecOutput {
121 stdout: sink.borrow().clone(),
122 stderr: e.to_string(),
123 duration: start.elapsed(),
124 exit: ExitStatus::Trap("js-error".into()),
125 format: OutputFormat::Text,
126 });
127 }
128 };
129
130 let mut stdout = sink.borrow().clone();
131 if stdout.is_empty() && !value.is_undefined() {
132 let s = value
133 .to_string(&mut context)
134 .map(|s| s.to_std_string_escaped())
135 .unwrap_or_else(|_| format!("{value:?}"));
136 stdout.push_str(&s);
137 }
138 Ok(ExecOutput {
139 stdout,
140 stderr: String::new(),
141 duration: start.elapsed(),
142 exit: ExitStatus::Ok,
143 format: OutputFormat::Text,
144 })
145 }
146}
147
148#[cfg(feature = "lang-query")]
150fn js_value_to_query_params(
151 val: &JsValue,
152 ctx: &mut Context,
153) -> Result<super::query::QueryParams, String> {
154 let obj = val.as_object().ok_or("outl.query expects an object")?;
155 let mut params = super::query::QueryParams::default();
156 if let Some(v) = obj
157 .get(js_string!("status"), ctx)
158 .map_err(|e| e.to_string())?
159 .as_string()
160 {
161 params.status = Some(v.to_std_string_escaped());
162 }
163 if let Some(v) = obj
164 .get(js_string!("tag"), ctx)
165 .map_err(|e| e.to_string())?
166 .as_string()
167 {
168 params.tag = Some(v.to_std_string_escaped());
169 }
170 if let Some(v) = obj
171 .get(js_string!("kind"), ctx)
172 .map_err(|e| e.to_string())?
173 .as_string()
174 {
175 params.kind = Some(v.to_std_string_escaped());
176 }
177 if let Some(v) = obj
178 .get(js_string!("since"), ctx)
179 .map_err(|e| e.to_string())?
180 .as_string()
181 {
182 params.since = Some(v.to_std_string_escaped());
183 }
184 if let Some(v) = obj
185 .get(js_string!("text"), ctx)
186 .map_err(|e| e.to_string())?
187 .as_string()
188 {
189 params.text = Some(v.to_std_string_escaped());
190 }
191 if let Some(v) = obj
192 .get(js_string!("limit"), ctx)
193 .map_err(|e| e.to_string())?
194 .as_number()
195 {
196 if v.is_finite() && v >= 0.0 {
197 params.limit = Some(v as usize);
198 }
199 }
200 let sort_val = obj
201 .get(js_string!("sort"), ctx)
202 .map_err(|e| e.to_string())?;
203 if let Some(s) = sort_val.as_string() {
204 let raw = s.to_std_string_escaped();
205 for part in raw.split(',') {
206 let trimmed = part.trim();
207 if !trimmed.is_empty() {
208 params.sort.push(trimmed.to_string());
209 }
210 }
211 }
212 Ok(params)
213}
214
215#[cfg(feature = "lang-query")]
217fn hits_to_js_array(hits: &[super::query::QueryHit], ctx: &mut Context) -> Result<JsValue, String> {
218 let arr = boa_engine::object::ObjectInitializer::new(ctx).build();
219 for (i, hit) in hits.iter().enumerate() {
220 let obj = boa_engine::object::ObjectInitializer::new(ctx)
221 .property(
222 js_string!("handle"),
223 js_string!(hit.handle.as_str()),
224 boa_engine::property::Attribute::all(),
225 )
226 .property(
227 js_string!("text"),
228 js_string!(hit.text.as_str()),
229 boa_engine::property::Attribute::all(),
230 )
231 .property(
232 js_string!("page"),
233 js_string!(hit.page.as_str()),
234 boa_engine::property::Attribute::all(),
235 )
236 .property(
237 js_string!("status"),
238 match hit.status.as_deref() {
239 Some(s) => js_string!(s).into(),
240 None => JsValue::null(),
241 },
242 boa_engine::property::Attribute::all(),
243 )
244 .build();
245 arr.set(i, obj, true, ctx).map_err(|e| e.to_string())?;
246 }
247 Ok(arr.into())
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 fn run(src: &str) -> String {
255 JsRuntime
256 .execute(src, &ExecContext::default())
257 .unwrap()
258 .stdout
259 }
260
261 #[test]
262 fn arithmetic_last_value_auto_printed() {
263 assert_eq!(run("1 + 2"), "3");
264 }
265
266 #[test]
267 fn console_log_writes_stdout() {
268 assert_eq!(run("console.log('hello')"), "hello\n");
269 }
270
271 #[test]
272 fn template_literals() {
273 assert_eq!(run("`x=${2+3}`"), "x=5");
274 }
275
276 #[test]
277 fn arrow_fn_and_map() {
278 assert_eq!(run("[1,2,3].map(n => n * n).join(',')"), "1,4,9");
279 }
280
281 #[test]
282 fn parse_error_returns_trap() {
283 let out = JsRuntime
284 .execute("function (", &ExecContext::default())
285 .unwrap();
286 assert!(matches!(out.exit, ExitStatus::Trap(_)));
287 assert!(!out.stderr.is_empty());
288 }
289}