Rust
Two crates make up the Rust API:
texl_corerenders templates and loads documents. Most programs only need itsEngine.swan_coreimplements the Swan language. You use itsValuetype to pass data to templates, and you can use it on its own to evaluate expressions.
This page is a guide. The API reference generated by rustdoc documents every type and function.
Adding the dependencies
The crates are not published on crates.io yet. Add them from the repository:
[dependencies]
texl-core = { git = "https://github.com/onlabsorg/texl" }
swan-core = { git = "https://github.com/onlabsorg/texl" }
tokio = { version = "1", features = ["rt", "macros", "fs", "time"] }
url = "2"
The examples on this page use tokio and url, but the engine works with any async runtime, or with none.
Rendering a template
use swan_core::value::Value;
use texl_core::{Engine, helpers::spawner_fn};
#[tokio::main(flavor = "current_thread")]
async fn main() {
// The engine is single-threaded: run it on a LocalSet.
tokio::task::LocalSet::new().run_until(async {
let engine = Engine::new(Some(spawner_fn(|task| {
tokio::task::spawn_local(task);
})));
engine.add_builtin("name", Value::Text("Ada".into()));
let text = engine.render("Hello <% name %>!").await;
assert_eq!(text, "Hello Ada!");
}).await;
}
The engine and Swan values use Rc, so they are not Send, and rendering
futures must run on one thread. With tokio, that means a LocalSet and
spawn_local. The spawner passed to Engine::new is how the engine starts
background tasks, such as imports.
Loading documents
The engine does not know how to read documents by itself. You register loaders: async functions that receive a URI and return the source text, or an error message.
use swan_core::value::Value;
use texl_core::{Engine, helpers::spawner_fn, loader_fn};
use url::Url;
async fn read_file(uri: String) -> Result<String, String> {
let path = Url::parse(&uri)
.ok()
.and_then(|url| url.to_file_path().ok())
.ok_or_else(|| format!("{uri}: not a file URL"))?;
tokio::fs::read_to_string(&path).await.map_err(|e| e.to_string())
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
tokio::task::LocalSet::new().run_until(async {
let engine = Engine::new(Some(spawner_fn(|task| {
tokio::task::spawn_local(task);
})));
engine.add_loader("file", loader_fn(read_file));
let path = std::fs::canonicalize("report.texl").unwrap();
let url = Url::from_file_path(&path).unwrap();
match engine.load(url.as_str()).await {
Value::Namespace(doc) => match doc.get("rendering") {
Value::Text(text) => print!("{text}"),
_ => unreachable!("a document's rendering is always text"),
},
_ => eprintln!("report.texl could not be loaded"),
}
}).await;
}
add_loader(scheme, loader)handles the URIs with that scheme, such asfileorhttps.add_default_loader(loader)handles URIs without a scheme. The engine normalizes them into absolute paths, such as/docs/report.texl.
Engine::load returns the document as a namespace with the members uri,
source, data and rendering, or Nothing if no loader matches or the
loader fails. Load a document by an absolute URI, so that the imports inside
it resolve against its location. The same loaders serve texl.import in
templates, and the engine caches every document it loads.
Value has no Display implementation: match on the variant to get the
text, as above.
When the host already has the source of a document, it can render it without
a loader: create a scope with the document's URI, with
engine.create_scope(Some(uri)), and pass it to template::render. The
imports inside it resolve against the URI, as if the document had been loaded.
Globals and host functions
add_builtin binds a global, visible to every template and document of the
engine. Any Value works,
including namespaces and functions:
use swan_core::value::{BuiltinFn, Scope, Value};
use texl_core::{Engine, helpers::spawner_fn};
#[tokio::main(flavor = "current_thread")]
async fn main() {
tokio::task::LocalSet::new().run_until(async {
let engine = Engine::new(Some(spawner_fn(|task| {
tokio::task::spawn_local(task);
})));
// A namespace: {name: "Ada", tags: ["admin", "dev"]}
let user = Scope::new();
user.set("name", Value::Text("Ada".into()));
user.set("tags", Value::list(vec![Value::Text("admin".into()), Value::Text("dev".into())]));
engine.add_builtin("user", Value::Namespace(user));
// A function. Several arguments arrive as one Value::Tuple.
engine.add_builtin("repeat", Value::Builtin(BuiltinFn::new("repeat", |arg| {
let items: Vec<Value> = arg.items().collect();
match items.as_slice() {
[Value::Text(t), Value::Numb(n)] => Value::Text(t.repeat(*n as usize).into()),
_ => Value::undef_unary("repeat", arg, 0),
}
})));
let text = engine.render("<% user.name %> <% repeat (\"*\", 3) %>").await;
assert_eq!(text, "Ada ***");
}).await;
}
A host function receives its whole argument as one Value. It must not
panic: return an Undefined value, for example with Value::undef_unary, when
the argument is invalid. Value::items
iterates over the items of a tuple, and treats any other value as a tuple of
one item.
Asynchronous host functions
A host function that needs to wait returns a promise.
helpers::promise_async
turns a future into one, and the template waits for it:
use std::time::Duration;
use swan_core::value::{BuiltinFn, Value};
use texl_core::{Engine, helpers::{promise_async, spawner_fn, SpawnFn}};
#[tokio::main(flavor = "current_thread")]
async fn main() {
tokio::task::LocalSet::new().run_until(async {
let spawner: SpawnFn = spawner_fn(|task| {
tokio::task::spawn_local(task);
});
let engine = Engine::new(Some(spawner.clone()));
engine.add_builtin("slow_double", Value::Builtin(BuiltinFn::new("slow_double", move |arg| {
promise_async(Some(&spawner), async move {
tokio::time::sleep(Duration::from_millis(10)).await;
match arg {
Value::Numb(n) => Value::Numb(n * 2.0),
other => Value::undef_unary("slow_double", other, 0),
}
})
})));
assert_eq!(engine.render("<% slow_double 21 %>").await, "42");
}).await;
}
Keeping names between renders
Engine::render uses a fresh scope each time. To render several templates in
the same scope, create one with Engine::create_scope and call
template::render. Its argument is the URI of the document the scope stands
for, which texl.import resolves relative URIs against; None resolves them
against the root path /, as in Engine::render:
use swan_core::value::Value;
use texl_core::{Engine, helpers::spawner_fn, template};
#[tokio::main(flavor = "current_thread")]
async fn main() {
tokio::task::LocalSet::new().run_until(async {
let engine = Engine::new(Some(spawner_fn(|task| {
tokio::task::spawn_local(task);
})));
let scope = engine.create_scope(None);
template::render("<% total = 6 * 7 %>", &scope).await;
assert_eq!(scope.get("total"), Value::Numb(42.0));
assert_eq!(template::render("Total: <% total %>", &scope).await, "Total: 42");
}).await;
}
Without an async runtime
Pass None as the spawner, and the engine puts its background tasks on a
thread-local queue. Drive the rendering future yourself, and call
helpers::run_pending to
run the queued tasks:
use std::{future::Future, pin::pin, task::{Context, Poll, Waker}};
use texl_core::{Engine, helpers::run_pending};
fn block_on<F: Future>(future: F) -> F::Output {
let mut cx = Context::from_waker(Waker::noop());
let mut future = pin!(future);
loop {
run_pending();
if let Poll::Ready(value) = future.as_mut().poll(&mut cx) {
return value;
}
}
}
fn main() {
let engine = Engine::new(None);
assert_eq!(block_on(engine.render("<% 6 * 7 %>")), "42");
}
This busy loop is fine for tests and tools whose loaders do not really wait. For real I/O, use a runtime.
Evaluating Swan expressions
swan_core evaluates Swan expressions without templates:
use swan_core::builtins::root_scope;
use swan_core::evaluator::eval;
use swan_core::lexer::Lexer;
use swan_core::parser::Parser;
use swan_core::value::Value;
fn main() {
let scope = root_scope();
let ast = Parser::new(Lexer::new("(1, 2, 3) => (x -> x * 10)")).parse();
let value = eval(&ast, &scope);
let items: Vec<Value> = value.items().collect();
assert_eq!(items, vec![Value::Numb(10.0), Value::Numb(20.0), Value::Numb(30.0)]);
}
Evaluation never fails and never panics: errors are Value::Undefined values.
See the swan_core documentation for the
pipeline and the value model.