#![recursion_limit = "1000"]
#![allow(clippy::comparison_chain)]
#![allow(clippy::wildcard_in_or_patterns)]
#![allow(clippy::manual_range_contains)]
extern crate self as typst;
pub mod diag;
pub mod engine;
pub mod eval;
pub mod foundations;
pub mod introspection;
pub mod layout;
pub mod loading;
pub mod math;
pub mod model;
pub mod realize;
pub mod symbols;
pub mod text;
pub mod visualize;
#[doc(inline)]
pub use typst_syntax as syntax;
#[doc(inline)]
pub use typst_utils as utils;
use std::collections::HashSet;
use std::ops::{Deref, Range};
use comemo::{Track, Tracked, Validate};
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_timing::{timed, TimingScope};
use crate::diag::{
warning, FileError, FileResult, SourceDiagnostic, SourceResult, Warned,
};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
Array, Bytes, Datetime, Dict, Module, Scope, StyleChain, Styles, Value,
};
use crate::introspection::Introspector;
use crate::layout::{Alignment, Dir};
use crate::model::Document;
use crate::syntax::package::PackageSpec;
use crate::syntax::{FileId, Source, Span};
use crate::text::{Font, FontBook};
use crate::utils::LazyHash;
use crate::visualize::Color;
#[typst_macros::time]
pub fn compile(world: &dyn World) -> Warned<SourceResult<Document>> {
let mut sink = Sink::new();
let output = compile_impl(world.track(), Traced::default().track(), &mut sink)
.map_err(deduplicate);
Warned { output, warnings: sink.warnings() }
}
#[typst_macros::time]
pub fn trace(world: &dyn World, span: Span) -> EcoVec<(Value, Option<Styles>)> {
let mut sink = Sink::new();
let traced = Traced::new(span);
compile_impl(world.track(), traced.track(), &mut sink).ok();
sink.values()
}
fn compile_impl(
world: Tracked<dyn World + '_>,
traced: Tracked<Traced>,
sink: &mut Sink,
) -> SourceResult<Document> {
let library = world.library();
let styles = StyleChain::new(&library.styles);
let main = world.main();
let main = world
.source(main)
.map_err(|err| hint_invalid_main_file(world, err, main))?;
let content = crate::eval::eval(
world,
traced,
sink.track_mut(),
Route::default().track(),
&main,
)?
.content();
let mut iter = 0;
let mut subsink;
let mut document = Document::default();
loop {
const ITER_NAMES: &[&str] =
&["layout (1)", "layout (2)", "layout (3)", "layout (4)", "layout (5)"];
let _scope = TimingScope::new(ITER_NAMES[iter], None);
subsink = Sink::new();
let constraint = <Introspector as Validate>::Constraint::new();
let mut engine = Engine {
world,
introspector: document.introspector.track_with(&constraint),
traced,
sink: subsink.track_mut(),
route: Route::default(),
};
document = crate::layout::layout_document(&mut engine, &content, styles)?;
iter += 1;
if timed!("check stabilized", document.introspector.validate(&constraint)) {
break;
}
if iter >= 5 {
subsink.warn(warning!(
Span::detached(), "layout did not converge within 5 attempts";
hint: "check if any states or queries are updating themselves"
));
break;
}
}
sink.extend_from_sink(subsink);
let delayed = sink.delayed();
if !delayed.is_empty() {
return Err(delayed);
}
Ok(document)
}
fn deduplicate(mut diags: EcoVec<SourceDiagnostic>) -> EcoVec<SourceDiagnostic> {
let mut unique = HashSet::new();
diags.retain(|diag| {
let hash = crate::utils::hash128(&(&diag.span, &diag.message));
unique.insert(hash)
});
diags
}
#[comemo::track]
pub trait World: Send + Sync {
fn library(&self) -> &LazyHash<Library>;
fn book(&self) -> &LazyHash<FontBook>;
fn main(&self) -> FileId;
fn source(&self, id: FileId) -> FileResult<Source>;
fn file(&self, id: FileId) -> FileResult<Bytes>;
fn font(&self, index: usize) -> Option<Font>;
fn today(&self, offset: Option<i64>) -> Option<Datetime>;
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
&[]
}
}
macro_rules! delegate_for_ptr {
($W:ident for $ptr:ty) => {
impl<$W: World> World for $ptr {
fn library(&self) -> &LazyHash<Library> {
self.deref().library()
}
fn book(&self) -> &LazyHash<FontBook> {
self.deref().book()
}
fn main(&self) -> FileId {
self.deref().main()
}
fn source(&self, id: FileId) -> FileResult<Source> {
self.deref().source(id)
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.deref().file(id)
}
fn font(&self, index: usize) -> Option<Font> {
self.deref().font(index)
}
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
self.deref().today(offset)
}
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
self.deref().packages()
}
}
};
}
delegate_for_ptr!(W for std::boxed::Box<W>);
delegate_for_ptr!(W for std::sync::Arc<W>);
delegate_for_ptr!(W for &W);
pub trait WorldExt {
fn range(&self, span: Span) -> Option<Range<usize>>;
}
impl<T: World> WorldExt for T {
fn range(&self, span: Span) -> Option<Range<usize>> {
self.source(span.id()?).ok()?.range(span)
}
}
#[derive(Debug, Clone, Hash)]
pub struct Library {
pub global: Module,
pub math: Module,
pub styles: Styles,
pub std: Value,
}
impl Library {
pub fn builder() -> LibraryBuilder {
LibraryBuilder::default()
}
}
impl Default for Library {
fn default() -> Self {
Self::builder().build()
}
}
#[derive(Debug, Clone, Default)]
pub struct LibraryBuilder {
inputs: Option<Dict>,
}
impl LibraryBuilder {
pub fn with_inputs(mut self, inputs: Dict) -> Self {
self.inputs = Some(inputs);
self
}
pub fn build(self) -> Library {
let math = math::module();
let inputs = self.inputs.unwrap_or_default();
let global = global(math.clone(), inputs);
let std = Value::Module(global.clone());
Library { global, math, styles: Styles::new(), std }
}
}
fn global(math: Module, inputs: Dict) -> Module {
let mut global = Scope::deduplicating();
self::foundations::define(&mut global, inputs);
self::model::define(&mut global);
self::text::define(&mut global);
global.reset_category();
global.define_module(math);
self::layout::define(&mut global);
self::visualize::define(&mut global);
self::introspection::define(&mut global);
self::loading::define(&mut global);
self::symbols::define(&mut global);
prelude(&mut global);
Module::new("global", global)
}
fn prelude(global: &mut Scope) {
global.reset_category();
global.define("black", Color::BLACK);
global.define("gray", Color::GRAY);
global.define("silver", Color::SILVER);
global.define("white", Color::WHITE);
global.define("navy", Color::NAVY);
global.define("blue", Color::BLUE);
global.define("aqua", Color::AQUA);
global.define("teal", Color::TEAL);
global.define("eastern", Color::EASTERN);
global.define("purple", Color::PURPLE);
global.define("fuchsia", Color::FUCHSIA);
global.define("maroon", Color::MAROON);
global.define("red", Color::RED);
global.define("orange", Color::ORANGE);
global.define("yellow", Color::YELLOW);
global.define("olive", Color::OLIVE);
global.define("green", Color::GREEN);
global.define("lime", Color::LIME);
global.define("luma", Color::luma_data());
global.define("oklab", Color::oklab_data());
global.define("oklch", Color::oklch_data());
global.define("rgb", Color::rgb_data());
global.define("cmyk", Color::cmyk_data());
global.define("range", Array::range_data());
global.define("ltr", Dir::LTR);
global.define("rtl", Dir::RTL);
global.define("ttb", Dir::TTB);
global.define("btt", Dir::BTT);
global.define("start", Alignment::START);
global.define("left", Alignment::LEFT);
global.define("center", Alignment::CENTER);
global.define("right", Alignment::RIGHT);
global.define("end", Alignment::END);
global.define("top", Alignment::TOP);
global.define("horizon", Alignment::HORIZON);
global.define("bottom", Alignment::BOTTOM);
}
fn hint_invalid_main_file(
world: Tracked<dyn World + '_>,
file_error: FileError,
input: FileId,
) -> EcoVec<SourceDiagnostic> {
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8);
let mut diagnostic =
SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));
if is_utf8_error {
let path = input.vpath();
let extension = path.as_rootless_path().extension();
if extension.is_some_and(|extension| extension == "typ") {
return eco_vec![diagnostic];
}
match extension {
Some(extension) => {
diagnostic.hint(eco_format!(
"a file with the `.{}` extension is not usually a Typst file",
extension.to_string_lossy()
));
}
None => {
diagnostic
.hint("a file without an extension is not usually a Typst file");
}
};
if world.source(input.with_extension("typ")).is_ok() {
diagnostic.hint("check if you meant to use the `.typ` extension instead");
}
}
eco_vec![diagnostic]
}