Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust

Two crates make up the Rust API:

  • texl_core renders templates and loads documents. Most programs only need its Engine.
  • swan_core implements the Swan language. You use its Value type 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 as file or https.
  • 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.

API reference

  • texl_core: Engine, template::render, loaders and the async helpers.
  • swan_core: Value, Scope, Promise, BuiltinFn, the lexer, the parser and the evaluator.