1use std::cell::RefCell;
69use std::collections::HashMap;
70use std::future::Future;
71use std::pin::Pin;
72use std::rc::Rc;
73use url::Url;
74
75use swan_core::builtins::root_scope;
76use swan_core::value::{BuiltinFn, Scope, Value};
77
78use crate::helpers::{promise_async, SpawnFn};
79use crate::template;
80
81pub type LoaderFn = Rc<dyn Fn(&str) -> Pin<Box<dyn Future<Output = Result<String, String>>>>>;
91
92pub fn loader_fn<F, Fut>(f: F) -> LoaderFn
104where
105 F: Fn(String) -> Fut + 'static,
106 Fut: Future<Output = Result<String, String>> + 'static,
107{
108 Rc::new(move |uri: &str| Box::pin(f(uri.to_string())))
109}
110
111#[derive(Clone)]
116pub struct Engine {
117 base_scope: Scope,
118 loaders: Rc<RefCell<HashMap<String, LoaderFn>>>,
119 default_loader: Rc<RefCell<Option<LoaderFn>>>,
120 cache: Rc<RefCell<HashMap<String, Value>>>,
121 spawner: Rc<RefCell<Option<SpawnFn>>>,
122}
123
124impl Engine {
125 pub fn new(spawner: Option<SpawnFn>) -> Self {
133 Self {
134 base_scope: root_scope(),
135 loaders: Rc::new(RefCell::new(HashMap::new())),
136 default_loader: Rc::new(RefCell::new(None)),
137 cache: Rc::new(RefCell::new(HashMap::new())),
138 spawner: Rc::new(RefCell::new(spawner)),
139 }
140 }
141
142 pub fn add_builtin(&self, name: &str, value: Value) {
148 self.base_scope.set(name, value);
149 }
150
151 pub fn add_loader(&self, protocol: &str, loader: LoaderFn) {
155 let scheme = protocol.to_lowercase();
156 self.loaders.borrow_mut().insert(scheme, loader);
157 }
158
159 pub fn add_default_loader(&self, loader: LoaderFn) {
162 *self.default_loader.borrow_mut() = Some(loader);
163 }
164
165 pub async fn load(&self, uri: &str) -> Value {
205 let norm_url = match self.normalize_url(uri) {
206 Ok(url) => url,
207 Err(err) => return Value::undef_unary("import.invalid_uri", Value::Text(err.into()), 0),
208 };
209 let doc_scope = self.create_scope(Some(norm_url.clone()));
211 self.load_internal(doc_scope, norm_url).await
212 }
213
214 pub async fn render(&self, template: &str) -> String {
238 let scope = self.create_scope(None);
239 template::render(template, &scope).await
240 }
241
242 pub fn create_scope(&self, base_url: Option<String>) -> Scope {
282 let child = self.base_scope.new_child();
284
285 let base_url = base_url.map(|url| self.normalize_url(&url).unwrap_or(url));
287 self.bind_texl_namespace(&child, base_url);
288
289 child
290 }
291
292 fn bind_texl_namespace(&self, scope: &Scope, base_url: Option<String>) {
297 let texl_ns = Scope::new();
298 texl_ns.set("import", self.create_import_builtin(scope.clone(), base_url));
299 scope.set("texl", Value::Namespace(texl_ns));
300 }
301
302 fn create_import_builtin(&self, parent_scope: Scope, base_url: Option<String>) -> Value {
304 let engine = self.clone();
305 Value::Builtin(BuiltinFn::new("texl.import", move |arg| {
306 match arg {
307 Value::Text(uri) => {
308 let norm_url = match engine.resolve_url(&uri, base_url.as_deref()) {
309 Ok(url) => url,
310 Err(err) => return Value::undef_unary("import.invalid_uri", Value::Text(err.into()), 0),
311 };
312
313 if let Some(cached) = engine.cache.borrow().get(&norm_url) {
315 return if matches!(cached, Value::Promise(_)) {
318 cached.clone()
319 } else {
320 let p = swan_core::value::Promise::new();
321 p.resolve(cached.clone());
322 Value::Promise(p)
323 };
324 }
325
326 let engine_clone = engine.clone();
327 let norm_url_key = norm_url.clone();
328 let parent_scope_clone = parent_scope.clone();
329
330 let spawner = engine.spawner.borrow().clone();
331 let promise = promise_async(
332 spawner.as_ref(),
333 async move {
334 let doc_scope = parent_scope_clone.new_child();
336
337 engine_clone.bind_texl_namespace(&doc_scope, Some(norm_url_key.clone()));
340
341 engine_clone.load_internal(doc_scope, norm_url_key).await
342 },
343 );
344
345 engine.cache.borrow_mut().insert(norm_url, promise.clone());
346 promise
347 }
348 other => Value::undef_unary("texl.import", other, 0),
349 }
350 }))
351 }
352
353 async fn load_internal(&self, doc_scope: Scope, norm_url: String) -> Value {
356 let loader = if let Ok(parsed) = Url::parse(&norm_url) {
357 let scheme = parsed.scheme().to_lowercase();
358 let l = self.loaders.borrow().get(&scheme).cloned();
359 match l {
360 Some(loader) => loader,
361 None => {
362 self.cache.borrow_mut().remove(&norm_url);
363 return Value::Nothing; }
365 }
366 } else {
367 let l = self.default_loader.borrow().clone();
368 match l {
369 Some(loader) => loader,
370 None => {
371 self.cache.borrow_mut().remove(&norm_url);
372 return Value::Nothing;
373 }
374 }
375 };
376
377 match loader(&norm_url).await {
378 Ok(source) => {
379 let rendering = template::render(&source, &doc_scope).await;
380
381 let result = Scope::new();
382 result.set("uri", Value::Text(norm_url.clone().into()));
383 result.set("source", Value::Text(source.into()));
384 result.set("data", Value::Namespace(doc_scope));
385 result.set("rendering", Value::Text(rendering.into()));
386
387 let result_val = Value::Namespace(result);
388
389 self.cache.borrow_mut().insert(norm_url, result_val.clone());
391 result_val
392 }
393 Err(_) => {
394 self.cache.borrow_mut().remove(&norm_url);
395 Value::Nothing
396 }
397 }
398 }
399 fn resolve_url(&self, uri: &str, base_url: Option<&str>) -> Result<String, String> {
404 let base = match base_url {
405 Some(base) if Url::parse(uri).is_err() => base,
406 _ => return self.normalize_url(uri),
407 };
408
409 if let Ok(base) = Url::parse(base) {
410 base.join(uri).map(|u| u.to_string()).map_err(|e| e.to_string())
412 } else if uri.starts_with('/') {
413 self.normalize_url(uri)
414 } else {
415 let dummy_base = Url::parse(&format!("app:///{}", base.trim_start_matches('/')))
417 .map_err(|e| e.to_string())?;
418 let joined = dummy_base.join(uri).map_err(|e| e.to_string())?;
419 let mut norm = joined.path().to_string();
420 if let Some(q) = joined.query() {
421 norm.push('?');
422 norm.push_str(q);
423 }
424 Ok(norm)
425 }
426 }
427
428 fn normalize_url(&self, uri: &str) -> Result<String, String> {
429 if let Ok(parsed) = Url::parse(uri) {
430 Ok(parsed.to_string())
431 } else {
432 if let Ok(dummy) = Url::parse(&format!("app:///{}", uri.trim_start_matches('/'))) {
433 let mut norm = dummy.path().to_string();
434 if let Some(q) = dummy.query() {
435 norm.push('?');
436 norm.push_str(q);
437 }
438 Ok(norm)
439 } else {
440 Ok(uri.trim().to_string())
441 }
442 }
443 }
444}
445
446
447
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use std::cell::Cell;
453 use std::pin::pin;
454 use std::sync::Arc;
455 use std::task::{Context, Poll, Wake, Waker};
456 use swan_core::value::Value;
457
458 struct NoopWake;
461 impl Wake for NoopWake {
462 fn wake(self: Arc<Self>) {}
463 }
464
465 fn block_on<F: Future>(fut: F) -> F::Output {
467 let waker = Waker::from(Arc::new(NoopWake));
468 let mut cx = Context::from_waker(&waker);
469 let mut fut = pin!(fut);
470 loop {
471 crate::helpers::run_pending();
473
474 if let Poll::Ready(v) = fut.as_mut().poll(&mut cx) {
475 return v;
476 }
477 }
478 }
479
480 fn unwrap_namespace(val: Value) -> Scope {
482 match val {
483 Value::Namespace(ns) => ns,
484 other => panic!("Expected Namespace, got {:?}", other),
485 }
486 }
487
488 #[test]
491 fn test_engine_renders_basic_template() {
492 let engine = Engine::new(None);
493 let out = block_on(engine.render("Hello <% 10 + 20 %>!"));
494 assert_eq!(out, "Hello 30!");
495 }
496
497 #[test]
498 fn test_engine_injects_builtins() {
499 let engine = Engine::new(None);
500 engine.add_builtin("system_name", Value::Text("TexlCore".into()));
501 engine.add_builtin("version", Value::Numb(2.0));
502
503 let out = block_on(engine.render("<% system_name %> v<% version %>"));
504 assert_eq!(out, "TexlCore v2");
505 }
506
507 #[test]
508 fn test_engine_routes_loaders_by_scheme() {
509 let engine = Engine::new(None);
510
511 engine.add_loader("http", loader_fn(|uri| async move {
512 Ok(format!("HTTP source for {}", uri))
513 }));
514
515 engine.add_default_loader(loader_fn(|uri| async move {
516 Ok(format!("Default source for {}", uri))
517 }));
518
519 let http_res = block_on(engine.load("http://example.com/doc.texl"));
521 let http_ns = unwrap_namespace(http_res);
522 assert_eq!(http_ns.get("source"), Value::Text("HTTP source for http://example.com/doc.texl".into()));
523
524 let file_res = block_on(engine.load("/local/file.texl"));
526 let file_ns = unwrap_namespace(file_res);
527 assert_eq!(file_ns.get("source"), Value::Text("Default source for /local/file.texl".into()));
528 }
529
530 #[test]
531 fn test_engine_load_returns_complete_namespace() {
532 let engine = Engine::new(None);
533 engine.add_builtin("multiplier", Value::Numb(5.0));
534
535 engine.add_default_loader(loader_fn(|_| async move {
536 Ok("Result is <% result = 10 * multiplier, result %>".to_string())
537 }));
538
539 let load_result = block_on(engine.load("/math.texl"));
540 let ns = unwrap_namespace(load_result);
541
542 assert_eq!(ns.get("uri"), Value::Text("/math.texl".into()));
543 assert_eq!(ns.get("rendering"), Value::Text("Result is 50".into()));
544
545 let doc_data = unwrap_namespace(ns.get("data"));
547 assert_eq!(doc_data.get("result"), Value::Numb(50.0));
548 }
549
550 #[test]
551 fn test_engine_caches_internal_imports_and_deduplicates() {
552 let engine = Engine::new(None);
553 let call_count = Rc::new(Cell::new(0));
554 let c = call_count.clone();
555
556 engine.add_default_loader(loader_fn(move |uri| {
557 c.set(c.get() + 1);
558 let content = if uri.contains("main") {
559 r#"Main loads <% texl.import("/shared.texl") ~> (ns -> ns.rendering) %> twice: <% texl.import("/shared.texl") ~> (ns -> ns.rendering) %>"#
561 } else {
562 "Shared!"
563 };
564 let content_str = content.to_string();
565 async move { Ok(content_str) }
566 }));
567
568 let rendered = block_on(engine.render(r#"<% texl.import("/main.texl") ~> (ns -> ns.rendering) %>"#));
570
571 assert_eq!(rendered, "Main loads Shared! twice: Shared!");
572
573 assert_eq!(call_count.get(), 2);
575 }
576
577 fn engine_with_docs(docs: &[(&str, &str)]) -> (Engine, Rc<RefCell<Vec<String>>>) {
580 let engine = Engine::new(None);
581 let docs: Rc<HashMap<String, String>> = Rc::new(
582 docs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
583 );
584 let requested = Rc::new(RefCell::new(Vec::new()));
585
586 let make_loader = || {
587 let docs = docs.clone();
588 let requested = requested.clone();
589 loader_fn(move |uri| {
590 requested.borrow_mut().push(uri.clone());
591 let res = docs.get(&uri).cloned().ok_or(format!("not found: {}", uri));
592 async move { res }
593 })
594 };
595 engine.add_default_loader(make_loader());
596 engine.add_loader("http", make_loader());
597 (engine, requested)
598 }
599
600 const RENDERING: &str = "~> (ns -> ns.rendering)";
601
602 #[test]
603 fn test_engine_resolves_relative_imports_against_importing_document() {
604 let main = format!(r#"[main <% texl.import("parts/header.texl") {RENDERING} %>]"#);
605 let header = format!(r#"[header <% texl.import("../footer.texl") {RENDERING} %>]"#);
606 let footer = format!(r#"[footer <% texl.import("./parts/../parts/note.texl") {RENDERING} %>]"#);
607 let (engine, requested) = engine_with_docs(&[
608 ("/docs/main.texl", &main),
609 ("/docs/parts/header.texl", &header),
610 ("/docs/footer.texl", &footer),
611 ("/docs/parts/note.texl", "note"),
612 ]);
613
614 let out = block_on(engine.render(&format!(r#"<% texl.import("/docs/main.texl") {RENDERING} %>"#)));
615
616 assert_eq!(out, "[main [header [footer note]]]");
617 assert_eq!(
618 *requested.borrow(),
619 vec!["/docs/main.texl", "/docs/parts/header.texl", "/docs/footer.texl", "/docs/parts/note.texl"]
620 );
621 }
622
623 #[test]
624 fn test_engine_load_resolves_relative_imports_against_loaded_document() {
625 let main = format!(r#"<% texl.import("lib.texl") {RENDERING} %>"#);
626 let (engine, requested) = engine_with_docs(&[("/a/b/main.texl", &main), ("/a/b/lib.texl", "lib")]);
627
628 let ns = unwrap_namespace(block_on(engine.load("/a/b/main.texl")));
629
630 assert_eq!(ns.get("rendering"), Value::Text("lib".into()));
631 assert_eq!(*requested.borrow(), vec!["/a/b/main.texl", "/a/b/lib.texl"]);
632 }
633
634 #[test]
635 fn test_engine_absolute_paths_and_uris_ignore_importing_document() {
636 let main = format!(
637 r#"<% texl.import("/top.texl") {RENDERING} %>,<% texl.import("http://example.com/x/remote.texl") {RENDERING} %>"#
638 );
639 let (engine, requested) = engine_with_docs(&[
640 ("/docs/main.texl", &main),
641 ("/top.texl", "top"),
642 ("http://example.com/x/remote.texl", "remote"),
643 ]);
644
645 let out = block_on(engine.render(&format!(r#"<% texl.import("/docs/main.texl") {RENDERING} %>"#)));
646
647 assert_eq!(out, "top,remote");
648 assert_eq!(
649 *requested.borrow(),
650 vec!["/docs/main.texl", "/top.texl", "http://example.com/x/remote.texl"]
651 );
652 }
653
654 #[test]
655 fn test_engine_resolves_relative_imports_against_uri_documents() {
656 let main = format!(r#"<% texl.import("../lib/util.texl?v=1") {RENDERING} %>"#);
657 let (engine, requested) = engine_with_docs(&[
658 ("http://example.com/site/pages/main.texl", &main),
659 ("http://example.com/site/lib/util.texl?v=1", "util"),
660 ]);
661
662 let ns = unwrap_namespace(block_on(engine.load("http://example.com/site/pages/main.texl")));
663
664 assert_eq!(ns.get("rendering"), Value::Text("util".into()));
665 assert_eq!(
666 *requested.borrow(),
667 vec!["http://example.com/site/pages/main.texl", "http://example.com/site/lib/util.texl?v=1"]
668 );
669 }
670
671 #[test]
672 fn test_engine_resolves_absolute_paths_against_the_root_of_uri_documents() {
673 let main = format!(r#"<% texl.import("/lib/util.texl") {RENDERING} %>"#);
674 let (engine, requested) = engine_with_docs(&[
675 ("http://example.com/site/pages/main.texl", &main),
676 ("http://example.com/lib/util.texl", "util"),
677 ]);
678
679 let ns = unwrap_namespace(block_on(engine.load("http://example.com/site/pages/main.texl")));
680
681 assert_eq!(ns.get("rendering"), Value::Text("util".into()));
682 assert_eq!(
683 *requested.borrow(),
684 vec!["http://example.com/site/pages/main.texl", "http://example.com/lib/util.texl"]
685 );
686 }
687
688 #[test]
689 fn test_engine_relative_imports_without_document_resolve_against_root() {
690 let (engine, requested) = engine_with_docs(&[("/dir/doc.texl", "doc")]);
691
692 let out = block_on(engine.render(&format!(r#"<% texl.import("dir/doc.texl") {RENDERING} %>"#)));
693
694 assert_eq!(out, "doc");
695 assert_eq!(*requested.borrow(), vec!["/dir/doc.texl"]);
696 }
697
698 #[test]
699 fn test_engine_scope_with_base_url_resolves_relative_imports_against_it() {
700 let (engine, requested) = engine_with_docs(&[
701 ("/docs/lib.texl", "lib"),
702 ("/top.texl", "top"),
703 ("http://example.com/site/lib.texl", "remote lib"),
704 ("http://example.com/top.texl", "remote top"),
705 ]);
706 let source = format!(r#"<% texl.import("lib.texl") {RENDERING} %>,<% texl.import("/top.texl") {RENDERING} %>"#);
707
708 let scope = engine.create_scope(Some("docs/main.texl".to_string()));
710 assert_eq!(block_on(template::render(&source, &scope)), "lib,top");
711
712 let scope = engine.create_scope(Some("http://example.com/site/main.texl".to_string()));
713 assert_eq!(block_on(template::render(&source, &scope)), "remote lib,remote top");
714
715 assert_eq!(
716 *requested.borrow(),
717 vec!["/docs/lib.texl", "/top.texl", "http://example.com/site/lib.texl", "http://example.com/top.texl"]
718 );
719 }
720
721 #[test]
722 fn test_engine_scope_with_base_url_renders_like_the_loaded_document() {
723 let main = format!(r#"<% x = 1 %><% texl.import("lib.texl") {RENDERING} %>"#);
724 let (engine, requested) = engine_with_docs(&[("/a/main.texl", &main), ("/a/lib.texl", "lib <% x %>")]);
725
726 let scope = engine.create_scope(Some("/a/main.texl".to_string()));
727 let rendering = block_on(template::render(&main, &scope));
728 let ns = unwrap_namespace(block_on(engine.load("/a/main.texl")));
729
730 assert_eq!(rendering, "lib 1");
731 assert_eq!(ns.get("rendering"), Value::Text(rendering.into()));
732 assert_eq!(*requested.borrow(), vec!["/a/lib.texl", "/a/main.texl"]);
734 }
735
736 #[test]
737 fn test_engine_scope_without_base_url_resolves_relative_imports_against_root() {
738 let (engine, requested) = engine_with_docs(&[("/dir/doc.texl", "doc")]);
739
740 let scope = engine.create_scope(None);
741 let out = block_on(template::render(&format!(r#"<% texl.import("dir/doc.texl") {RENDERING} %>"#), &scope));
742
743 assert_eq!(out, "doc");
744 assert_eq!(*requested.borrow(), vec!["/dir/doc.texl"]);
745 }
746
747 #[test]
748 fn test_engine_caches_relative_imports_by_resolved_url() {
749 let a = format!(r#"<% texl.import("common.texl") {RENDERING} %>"#);
751 let b = format!(r#"<% texl.import("common.texl") {RENDERING} %>"#);
752 let c = format!(r#"<% texl.import("../a/common.texl") {RENDERING} %>"#);
754 let (engine, requested) = engine_with_docs(&[
755 ("/a/main.texl", &a),
756 ("/b/main.texl", &b),
757 ("/c/main.texl", &c),
758 ("/a/common.texl", "A"),
759 ("/b/common.texl", "B"),
760 ]);
761
762 let out = block_on(engine.render(&format!(
763 r#"<% texl.import("/a/main.texl") {RENDERING} %><% texl.import("/b/main.texl") {RENDERING} %><% texl.import("/c/main.texl") {RENDERING} %>"#
764 )));
765
766 assert_eq!(out, "ABA");
767 assert_eq!(
768 *requested.borrow(),
769 vec!["/a/main.texl", "/a/common.texl", "/b/main.texl", "/b/common.texl", "/c/main.texl"]
770 );
771 }
772
773 #[test]
774 fn test_engine_handles_missing_loader_gracefully() {
775 let engine = Engine::new(None);
776 let out = block_on(engine.load("ftp://example.com/file.texl"));
779
780 assert_eq!(out, Value::Nothing);
782 }
783}