diff --git a/bench/src/clock.rs b/bench/src/clock.rs index 0f684f853..b86b06dcd 100644 --- a/bench/src/clock.rs +++ b/bench/src/clock.rs @@ -11,7 +11,7 @@ use typst::export::pdf; use typst::layout::{layout, Frame, LayoutTree}; use typst::loading::FsLoader; use typst::parse::parse; -use typst::source::SourceFile; +use typst::source::SourceId; use typst::syntax::SyntaxTree; use typst::Context; @@ -21,15 +21,13 @@ const CASES: &[&str] = &["coma.typ", "text/basic.typ"]; fn benchmarks(c: &mut Criterion) { let loader = FsLoader::new().with_path(FONT_DIR).wrap(); - let ctx = Rc::new(RefCell::new(Context::new(loader.clone()))); + let ctx = Rc::new(RefCell::new(Context::new(loader))); for case in CASES { let path = Path::new(TYP_DIR).join(case); let name = path.file_stem().unwrap().to_string_lossy(); - let file = loader.resolve(&path).unwrap(); - let src = std::fs::read_to_string(&path).unwrap(); - let source = SourceFile::new(file, src); - let case = Case::new(ctx.clone(), source); + let id = ctx.borrow_mut().sources.load(&path).unwrap(); + let case = Case::new(ctx.clone(), id); macro_rules! bench { ($step:literal, setup = |$ctx:ident| $setup:expr, code = $code:expr $(,)?) => { @@ -82,7 +80,7 @@ fn benchmarks(c: &mut Criterion) { /// A test case with prepared intermediate results. struct Case { ctx: Rc>, - source: SourceFile, + id: SourceId, ast: Rc, module: Module, tree: LayoutTree, @@ -90,26 +88,23 @@ struct Case { } impl Case { - fn new(ctx: Rc>, source: SourceFile) -> Self { + fn new(ctx: Rc>, id: SourceId) -> Self { let mut borrowed = ctx.borrow_mut(); - let ast = Rc::new(parse(&source).unwrap()); - let module = eval(&mut borrowed, source.file(), Rc::clone(&ast)).unwrap(); + let source = borrowed.sources.get(id); + let ast = Rc::new(parse(source).unwrap()); + let module = eval(&mut borrowed, id, Rc::clone(&ast)).unwrap(); let tree = exec(&mut borrowed, &module.template); let frames = layout(&mut borrowed, &tree); drop(borrowed); - Self { ctx, source, ast, module, tree, frames } + Self { ctx, id, ast, module, tree, frames } } fn parse(&self) -> SyntaxTree { - parse(&self.source).unwrap() + parse(self.ctx.borrow().sources.get(self.id)).unwrap() } fn eval(&self) -> TypResult { - eval( - &mut self.ctx.borrow_mut(), - self.source.file(), - Rc::clone(&self.ast), - ) + eval(&mut self.ctx.borrow_mut(), self.id, Rc::clone(&self.ast)) } fn exec(&self) -> LayoutTree { @@ -121,7 +116,7 @@ impl Case { } fn typeset(&self) -> TypResult>> { - self.ctx.borrow_mut().typeset(&self.source) + self.ctx.borrow_mut().typeset(self.id) } fn pdf(&self) -> Vec { diff --git a/bench/src/parsing.rs b/bench/src/parsing.rs index dd5e12790..f95dfe75a 100644 --- a/bench/src/parsing.rs +++ b/bench/src/parsing.rs @@ -1,7 +1,6 @@ use iai::{black_box, main}; use typst::diag::TypResult; -use typst::loading::FileId; use typst::parse::{parse, Scanner, TokenMode, Tokens}; use typst::source::SourceFile; use typst::syntax::SyntaxTree; @@ -33,8 +32,7 @@ fn bench_tokenize() -> usize { } fn bench_parse() -> TypResult { - let source = SourceFile::new(FileId::from_raw(0), black_box(SRC).into()); - parse(&source) + parse(&SourceFile::detached(black_box(SRC))) } main!(bench_decode, bench_scan, bench_tokenize, bench_parse); diff --git a/src/diag.rs b/src/diag.rs index 397a833fb..0fcdef173 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; -use crate::loading::FileId; +use crate::source::SourceId; use crate::syntax::Span; /// The result type for typesetting and all its subpasses. @@ -14,14 +14,14 @@ pub type StrResult = Result; /// An error in a source file. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] pub struct Error { - /// The file that contains the error. - pub file: FileId, + /// The id of the source file that contains the error. + pub source: SourceId, /// The erroneous location in the source code. pub span: Span, /// A diagnostic message describing the problem. pub message: String, /// The trace of function calls leading to the error. - pub trace: Vec<(FileId, Span, Tracepoint)>, + pub trace: Vec<(SourceId, Span, Tracepoint)>, } /// A part of an error's [trace](Error::trace). @@ -35,9 +35,13 @@ pub enum Tracepoint { impl Error { /// Create a new, bare error. - pub fn new(file: FileId, span: impl Into, message: impl Into) -> Self { + pub fn new( + source: SourceId, + span: impl Into, + message: impl Into, + ) -> Self { Self { - file, + source, span: span.into(), trace: vec![], message: message.into(), @@ -47,11 +51,11 @@ impl Error { /// Create a boxed vector containing one error. The return value is suitable /// as the `Err` variant of a [`TypResult`]. pub fn boxed( - file: FileId, + source: SourceId, span: impl Into, message: impl Into, ) -> Box> { - Box::new(vec![Self::new(file, span, message)]) + Box::new(vec![Self::new(source, span, message)]) } /// Partially build a vec-boxed error, returning a function that just needs @@ -60,23 +64,23 @@ impl Error { /// This is useful in to convert from [`StrResult`] to a [`TypResult`] using /// [`map_err`](Result::map_err). pub fn partial( - file: FileId, + source: SourceId, span: impl Into, ) -> impl FnOnce(String) -> Box> { - move |message| Self::boxed(file, span, message) + move |message| Self::boxed(source, span, message) } } /// Early-return with a vec-boxed [`Error`]. #[macro_export] macro_rules! bail { - ($file:expr, $span:expr, $message:expr $(,)?) => { + ($source:expr, $span:expr, $message:expr $(,)?) => { return Err(Box::new(vec![$crate::diag::Error::new( - $file, $span, $message, + $source, $span, $message, )])); }; - ($file:expr, $span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => { - $crate::bail!($file, $span, format!($fmt, $($arg),+)); + ($source:expr, $span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => { + $crate::bail!($source, $span, format!($fmt, $($arg),+)); }; } diff --git a/src/eval/function.rs b/src/eval/function.rs index b9a168d25..8b1e883f0 100644 --- a/src/eval/function.rs +++ b/src/eval/function.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use super::{Cast, EvalContext, Value}; use crate::diag::{Error, TypResult}; -use crate::loading::FileId; +use crate::source::SourceId; use crate::syntax::{Span, Spanned}; use crate::util::EcoString; @@ -59,8 +59,8 @@ impl PartialEq for Function { /// Evaluated arguments to a function. #[derive(Debug, Clone, PartialEq)] pub struct FuncArgs { - /// The file in which the function was called. - pub file: FileId, + /// The id of the source file in which the function was called. + pub source: SourceId, /// The span of the whole argument list. pub span: Span, /// The positional arguments. @@ -103,7 +103,7 @@ impl FuncArgs { { match self.eat() { Some(found) => Ok(found), - None => bail!(self.file, self.span, "missing argument: {}", what), + None => bail!(self.source, self.span, "missing argument: {}", what), } } @@ -134,14 +134,14 @@ impl FuncArgs { let value = self.items.remove(index).value; let span = value.span; - T::cast(value).map(Some).map_err(Error::partial(self.file, span)) + T::cast(value).map(Some).map_err(Error::partial(self.source, span)) } /// Return an "unexpected argument" error if there is any remaining /// argument. pub fn finish(self) -> TypResult<()> { if let Some(arg) = self.items.first() { - bail!(self.file, arg.span, "unexpected argument"); + bail!(self.source, arg.span, "unexpected argument"); } Ok(()) } diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 8f5532eb1..22c7c0b43 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -21,30 +21,35 @@ pub use template::*; pub use value::*; use std::collections::HashMap; +use std::io; use std::mem; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; use crate::diag::{Error, StrResult, Tracepoint, TypResult}; use crate::geom::{Angle, Fractional, Length, Relative}; -use crate::image::ImageCache; -use crate::loading::{FileId, Loader}; +use crate::image::ImageStore; +use crate::loading::Loader; use crate::parse::parse; -use crate::source::{SourceFile, SourceMap}; +use crate::source::{SourceId, SourceStore}; use crate::syntax::visit::Visit; use crate::syntax::*; use crate::util::EcoString; use crate::Context; /// Evaluate a parsed source file into a module. -pub fn eval(ctx: &mut Context, file: FileId, ast: Rc) -> TypResult { - let mut ctx = EvalContext::new(ctx, file); +pub fn eval( + ctx: &mut Context, + source: SourceId, + ast: Rc, +) -> TypResult { + let mut ctx = EvalContext::new(ctx, source); let template = ast.eval(&mut ctx)?; Ok(Module { scope: ctx.scopes.top, template }) } /// Caches evaluated modules. -pub type ModuleCache = HashMap; +pub type ModuleCache = HashMap; /// An evaluated module, ready for importing or execution. #[derive(Debug, Clone, PartialEq)] @@ -68,100 +73,99 @@ pub trait Eval { pub struct EvalContext<'a> { /// The loader from which resources (files and images) are loaded. pub loader: &'a dyn Loader, - /// The store for source files. - pub sources: &'a mut SourceMap, - /// The cache for decoded images. - pub images: &'a mut ImageCache, - /// The cache for loaded modules. + /// Stores loaded source files. + pub sources: &'a mut SourceStore, + /// Stores decoded images. + pub images: &'a mut ImageStore, + /// Caches evaluated modules. pub modules: &'a mut ModuleCache, /// The active scopes. pub scopes: Scopes<'a>, - /// The currently evaluated file. - pub file: FileId, + /// The id of the currently evaluated source file. + pub source: SourceId, /// The stack of imported files that led to evaluation of the current file. - pub route: Vec, + pub route: Vec, /// The expression map for the currently built template. pub map: ExprMap, } impl<'a> EvalContext<'a> { /// Create a new evaluation context. - pub fn new(ctx: &'a mut Context, file: FileId) -> Self { + pub fn new(ctx: &'a mut Context, source: SourceId) -> Self { Self { loader: ctx.loader.as_ref(), sources: &mut ctx.sources, images: &mut ctx.images, modules: &mut ctx.modules, scopes: Scopes::new(Some(&ctx.std)), - file, + source, route: vec![], map: ExprMap::new(), } } - /// Resolve a path relative to the current file. - /// - /// Returns an error if the file is not found. - pub fn resolve(&mut self, path: &str, span: Span) -> TypResult { - self.loader - .resolve_from(self.file, Path::new(path)) - .map_err(|_| Error::boxed(self.file, span, "file not found")) - } - /// Process an import of a module relative to the current location. - pub fn import(&mut self, path: &str, span: Span) -> TypResult { - let file = self.resolve(path, span)?; + pub fn import(&mut self, path: &str, span: Span) -> TypResult { + // Load the source file. + let full = self.relpath(path); + let id = self.sources.load(&full).map_err(|err| { + Error::boxed(self.source, span, match err.kind() { + io::ErrorKind::NotFound => "file not found".into(), + _ => format!("failed to load source file ({})", err), + }) + })?; // Prevent cyclic importing. - if self.file == file || self.route.contains(&file) { - bail!(self.file, span, "cyclic import"); + if self.source == id || self.route.contains(&id) { + bail!(self.source, span, "cyclic import"); } // Check whether the module was already loaded. - if self.modules.get(&file).is_some() { - return Ok(file); + if self.modules.get(&id).is_some() { + return Ok(id); } - // Load the source file. - let buffer = self - .loader - .load_file(file) - .map_err(|_| Error::boxed(self.file, span, "failed to load file"))?; - - // Decode UTF-8. - let string = String::from_utf8(buffer) - .map_err(|_| Error::boxed(self.file, span, "file is not valid utf-8"))?; - // Parse the file. - let source = self.sources.insert(SourceFile::new(file, string)); + let source = self.sources.get(id); let ast = parse(&source)?; // Prepare the new context. let new_scopes = Scopes::new(self.scopes.base); let old_scopes = mem::replace(&mut self.scopes, new_scopes); - self.route.push(self.file); - self.file = file; + self.route.push(self.source); + self.source = id; // Evaluate the module. let result = Rc::new(ast).eval(self); // Restore the old context. let new_scopes = mem::replace(&mut self.scopes, old_scopes); - self.file = self.route.pop().unwrap(); + self.source = self.route.pop().unwrap(); // Add a tracepoint to the errors. let template = result.map_err(|mut errors| { for error in errors.iter_mut() { - error.trace.push((self.file, span, Tracepoint::Import)); + error.trace.push((self.source, span, Tracepoint::Import)); } errors })?; // Save the evaluated module. let module = Module { scope: new_scopes.top, template }; - self.modules.insert(file, module); + self.modules.insert(id, module); - Ok(file) + Ok(id) + } + + /// Complete a path that is relative to the current file to be relative to + /// the environment's current directory. + pub fn relpath(&self, path: impl AsRef) -> PathBuf { + self.sources + .get(self.source) + .path() + .parent() + .expect("is a file") + .join(path) } } @@ -231,7 +235,7 @@ impl Eval for Expr { Self::Str(_, ref v) => Value::Str(v.clone()), Self::Ident(ref v) => match ctx.scopes.get(&v) { Some(slot) => slot.borrow().clone(), - None => bail!(ctx.file, v.span, "unknown variable"), + None => bail!(ctx.source, v.span, "unknown variable"), }, Self::Array(ref v) => Value::Array(v.eval(ctx)?), Self::Dict(ref v) => Value::Dict(v.eval(ctx)?), @@ -300,7 +304,7 @@ impl Eval for BlockExpr { for expr in &self.exprs { let value = expr.eval(ctx)?; output = ops::join(output, value) - .map_err(Error::partial(ctx.file, expr.span()))?; + .map_err(Error::partial(ctx.source, expr.span()))?; } if self.scoping { @@ -321,7 +325,7 @@ impl Eval for UnaryExpr { UnOp::Neg => ops::neg(value), UnOp::Not => ops::not(value), }; - result.map_err(Error::partial(ctx.file, self.span)) + result.map_err(Error::partial(ctx.source, self.span)) } } @@ -368,7 +372,7 @@ impl BinaryExpr { } let rhs = self.rhs.eval(ctx)?; - op(lhs, rhs).map_err(Error::partial(ctx.file, self.span)) + op(lhs, rhs).map_err(Error::partial(ctx.source, self.span)) } /// Apply an assignment operation. @@ -380,22 +384,22 @@ impl BinaryExpr { let slot = if let Expr::Ident(id) = self.lhs.as_ref() { match ctx.scopes.get(id) { Some(slot) => Rc::clone(slot), - None => bail!(ctx.file, lspan, "unknown variable"), + None => bail!(ctx.source, lspan, "unknown variable"), } } else { - bail!(ctx.file, lspan, "cannot assign to this expression",); + bail!(ctx.source, lspan, "cannot assign to this expression",); }; let rhs = self.rhs.eval(ctx)?; let mut mutable = match slot.try_borrow_mut() { Ok(mutable) => mutable, Err(_) => { - bail!(ctx.file, lspan, "cannot assign to a constant",); + bail!(ctx.source, lspan, "cannot assign to a constant",); } }; let lhs = mem::take(&mut *mutable); - *mutable = op(lhs, rhs).map_err(Error::partial(ctx.file, self.span))?; + *mutable = op(lhs, rhs).map_err(Error::partial(ctx.source, self.span))?; Ok(Value::None) } @@ -409,18 +413,18 @@ impl Eval for CallExpr { .callee .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.file, self.callee.span()))?; + .map_err(Error::partial(ctx.source, self.callee.span()))?; let mut args = self.args.eval(ctx)?; let returned = callee(ctx, &mut args).map_err(|mut errors| { for error in errors.iter_mut() { // Skip errors directly related to arguments. - if error.file == ctx.file && self.span.contains(error.span) { + if error.source == ctx.source && self.span.contains(error.span) { continue; } error.trace.push(( - ctx.file, + ctx.source, self.span, Tracepoint::Call(callee.name().map(Into::into)), )); @@ -439,7 +443,7 @@ impl Eval for CallArgs { fn eval(&self, ctx: &mut EvalContext) -> TypResult { Ok(FuncArgs { - file: ctx.file, + source: ctx.source, span: self.span, items: self .items @@ -473,7 +477,7 @@ impl Eval for ClosureExpr { type Output = Value; fn eval(&self, ctx: &mut EvalContext) -> TypResult { - let file = ctx.file; + let file = ctx.source; let params = Rc::clone(&self.params); let body = Rc::clone(&self.body); @@ -489,7 +493,7 @@ impl Eval for ClosureExpr { // Don't leak the scopes from the call site. Instead, we use the // scope of captured variables we collected earlier. let prev_scopes = mem::take(&mut ctx.scopes); - let prev_file = mem::replace(&mut ctx.file, file); + let prev_file = mem::replace(&mut ctx.source, file); ctx.scopes.top = captured.clone(); for param in params.iter() { @@ -499,7 +503,7 @@ impl Eval for ClosureExpr { let result = body.eval(ctx); ctx.scopes = prev_scopes; - ctx.file = prev_file; + ctx.source = prev_file; result }); @@ -515,7 +519,7 @@ impl Eval for WithExpr { .callee .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.file, self.callee.span()))?; + .map_err(Error::partial(ctx.source, self.callee.span()))?; let applied = self.args.eval(ctx)?; @@ -565,7 +569,7 @@ impl Eval for IfExpr { .condition .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.file, self.condition.span()))?; + .map_err(Error::partial(ctx.source, self.condition.span()))?; if condition { self.if_body.eval(ctx) @@ -587,11 +591,11 @@ impl Eval for WhileExpr { .condition .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.file, self.condition.span()))? + .map_err(Error::partial(ctx.source, self.condition.span()))? { let value = self.body.eval(ctx)?; output = ops::join(output, value) - .map_err(Error::partial(ctx.file, self.body.span()))?; + .map_err(Error::partial(ctx.source, self.body.span()))?; } Ok(output) @@ -613,7 +617,7 @@ impl Eval for ForExpr { let value = self.body.eval(ctx)?; output = ops::join(output, value) - .map_err(Error::partial(ctx.file, self.body.span()))?; + .map_err(Error::partial(ctx.source, self.body.span()))?; } ctx.scopes.exit(); @@ -639,10 +643,10 @@ impl Eval for ForExpr { iter!(for (k => key, v => value) in dict.into_iter()) } (ForPattern::KeyValue(_, _), Value::Str(_)) => { - bail!(ctx.file, self.pattern.span(), "mismatched pattern"); + bail!(ctx.source, self.pattern.span(), "mismatched pattern"); } (_, iter) => bail!( - ctx.file, + ctx.source, self.iter.span(), "cannot loop over {}", iter.type_name(), @@ -659,7 +663,7 @@ impl Eval for ImportExpr { .path .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.file, self.path.span()))?; + .map_err(Error::partial(ctx.source, self.path.span()))?; let file = ctx.import(&path, self.path.span())?; let module = &ctx.modules[&file]; @@ -675,7 +679,7 @@ impl Eval for ImportExpr { if let Some(slot) = module.scope.get(&ident) { ctx.scopes.def_mut(ident.as_str(), slot.borrow().clone()); } else { - bail!(ctx.file, ident.span, "unresolved import"); + bail!(ctx.source, ident.span, "unresolved import"); } } } @@ -693,7 +697,7 @@ impl Eval for IncludeExpr { .path .eval(ctx)? .cast::() - .map_err(Error::partial(ctx.file, self.path.span()))?; + .map_err(Error::partial(ctx.source, self.path.span()))?; let file = ctx.import(&path, self.path.span())?; let module = &ctx.modules[&file]; diff --git a/src/export/pdf.rs b/src/export/pdf.rs index bfd364213..d4b3ac25c 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -14,17 +14,17 @@ use pdf_writer::{ use ttf_parser::{name_id, GlyphId}; use crate::color::Color; -use crate::font::{Em, FaceId, FontCache}; +use crate::font::{Em, FaceId, FontStore}; use crate::geom::{self, Length, Size}; -use crate::image::{Image, ImageCache, ImageId}; +use crate::image::{Image, ImageId, ImageStore}; use crate::layout::{Element, Frame, Geometry, Paint}; use crate::Context; /// Export a collection of frames into a PDF document. /// /// This creates one page per frame. In addition to the frames, you need to pass -/// in the cache used during compilation such that things like fonts and images -/// can be included in the PDF. +/// in the context used during compilation such that things like fonts and +/// images can be included in the PDF. /// /// Returns the raw bytes making up the PDF document. pub fn pdf(ctx: &Context, frames: &[Rc]) -> Vec { @@ -33,19 +33,16 @@ pub fn pdf(ctx: &Context, frames: &[Rc]) -> Vec { struct PdfExporter<'a> { writer: PdfWriter, - frames: &'a [Rc], - fonts: &'a FontCache, - font_map: Remapper, - images: &'a ImageCache, - image_map: Remapper, refs: Refs, + frames: &'a [Rc], + fonts: &'a FontStore, + images: &'a ImageStore, + font_map: Remapper, + image_map: Remapper, } impl<'a> PdfExporter<'a> { fn new(ctx: &'a Context, frames: &'a [Rc]) -> Self { - let mut writer = PdfWriter::new(1, 7); - writer.set_indent(2); - let mut font_map = Remapper::new(); let mut image_map = Remapper::new(); let mut alpha_masks = 0; @@ -66,14 +63,15 @@ impl<'a> PdfExporter<'a> { } } - let refs = Refs::new(frames.len(), font_map.len(), image_map.len(), alpha_masks); + let mut writer = PdfWriter::new(1, 7); + writer.set_indent(2); Self { writer, + refs: Refs::new(frames.len(), font_map.len(), image_map.len(), alpha_masks), frames, fonts: &ctx.fonts, images: &ctx.images, - refs, font_map, image_map, } diff --git a/src/font.rs b/src/font.rs index a609e934d..e756f84ec 100644 --- a/src/font.rs +++ b/src/font.rs @@ -3,13 +3,151 @@ use std::collections::{hash_map::Entry, HashMap}; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::Add; +use std::path::PathBuf; use std::rc::Rc; use decorum::N64; use serde::{Deserialize, Serialize}; use crate::geom::Length; -use crate::loading::{FileId, Loader}; +use crate::loading::{FileHash, Loader}; + +/// A unique identifier for a loaded font face. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +pub struct FaceId(u32); + +impl FaceId { + /// Create a face id from the raw underlying value. + /// + /// This should only be called with values returned by + /// [`into_raw`](Self::into_raw). + pub const fn from_raw(v: u32) -> Self { + Self(v) + } + + /// Convert into the raw underlying value. + pub const fn into_raw(self) -> u32 { + self.0 + } +} + +/// Storage for loaded and parsed font faces. +pub struct FontStore { + loader: Rc, + faces: Vec>, + families: HashMap>, + buffers: HashMap>>, + on_load: Option>, +} + +impl FontStore { + /// Create a new, empty font store. + pub fn new(loader: Rc) -> Self { + let mut faces = vec![]; + let mut families = HashMap::>::new(); + + for (i, info) in loader.faces().iter().enumerate() { + let id = FaceId(i as u32); + faces.push(None); + families + .entry(info.family.to_lowercase()) + .and_modify(|vec| vec.push(id)) + .or_insert_with(|| vec![id]); + } + + Self { + loader, + faces, + families, + buffers: HashMap::new(), + on_load: None, + } + } + + /// Register a callback which is invoked each time a font face is loaded. + pub fn on_load(&mut self, f: F) + where + F: Fn(FaceId, &Face) + 'static, + { + self.on_load = Some(Box::new(f)); + } + + /// Query for and load the font face from the given `family` that most + /// closely matches the given `variant`. + pub fn select(&mut self, family: &str, variant: FontVariant) -> Option { + // Check whether a family with this name exists. + let ids = self.families.get(family)?; + let infos = self.loader.faces(); + + let mut best = None; + let mut best_key = None; + + // Find the best matching variant of this font. + for &id in ids { + let current = infos[id.0 as usize].variant; + + // This is a perfect match, no need to search further. + if current == variant { + best = Some(id); + break; + } + + // If this is not a perfect match, we compute a key that we want to + // minimize among all variants. This key prioritizes style, then + // stretch distance and then weight distance. + let key = ( + current.style != variant.style, + current.stretch.distance(variant.stretch), + current.weight.distance(variant.weight), + ); + + if best_key.map_or(true, |b| key < b) { + best = Some(id); + best_key = Some(key); + } + } + + let id = best?; + + // Load the face if it's not already loaded. + let idx = id.0 as usize; + let slot = &mut self.faces[idx]; + if slot.is_none() { + let FaceInfo { ref path, index, .. } = infos[idx]; + + // Check the buffer cache since multiple faces may + // refer to the same data (font collection). + let hash = self.loader.resolve(path).ok()?; + let buffer = match self.buffers.entry(hash) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let buffer = self.loader.load(path).ok()?; + entry.insert(Rc::new(buffer)) + } + }; + + let face = Face::new(Rc::clone(buffer), index)?; + if let Some(callback) = &self.on_load { + callback(id, &face); + } + + *slot = Some(face); + } + + Some(id) + } + + /// Get a reference to a loaded face. + /// + /// This panics if no face with this id was loaded. This function should + /// only be called with ids returned by this store's + /// [`select()`](Self::select) method. + #[track_caller] + pub fn get(&self, id: FaceId) -> &Face { + self.faces[id.0 as usize].as_ref().expect("font face was not loaded") + } +} /// A font face. pub struct Face { @@ -53,18 +191,20 @@ impl Face { let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em); let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em); let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender())); - let strikeout = ttf.strikeout_metrics(); let underline = ttf.underline_metrics(); - let default = Em::new(0.06); let strikethrough = LineMetrics { - strength: strikeout.or(underline).map_or(default, |s| to_em(s.thickness)), + strength: strikeout + .or(underline) + .map_or(Em::new(0.06), |s| to_em(s.thickness)), position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)), }; let underline = LineMetrics { - strength: underline.or(strikeout).map_or(default, |s| to_em(s.thickness)), + strength: underline + .or(strikeout) + .map_or(Em::new(0.06), |s| to_em(s.thickness)), position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)), }; @@ -127,39 +267,6 @@ impl Face { } } -/// Identifies a vertical metric of a font. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum VerticalFontMetric { - /// The distance from the baseline to the typographic ascender. - /// - /// Corresponds to the typographic ascender from the `OS/2` table if present - /// and falls back to the ascender from the `hhea` table otherwise. - Ascender, - /// The approximate height of uppercase letters. - CapHeight, - /// The approximate height of non-ascending lowercase letters. - XHeight, - /// The baseline on which the letters rest. - Baseline, - /// The distance from the baseline to the typographic descender. - /// - /// Corresponds to the typographic descender from the `OS/2` table if - /// present and falls back to the descender from the `hhea` table otherwise. - Descender, -} - -impl Display for VerticalFontMetric { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Ascender => "ascender", - Self::CapHeight => "cap-height", - Self::XHeight => "x-height", - Self::Baseline => "baseline", - Self::Descender => "descender", - }) - } -} - /// A length in em units. /// /// `1em` is the same as the font size. @@ -201,137 +308,36 @@ impl Add for Em { } } -/// Caches parsed font faces. -pub struct FontCache { - loader: Rc, - faces: Vec>, - families: HashMap>, - buffers: HashMap>>, - on_load: Option>, -} - -impl FontCache { - /// Create a new, empty font cache. - pub fn new(loader: Rc) -> Self { - let mut faces = vec![]; - let mut families = HashMap::>::new(); - - for (i, info) in loader.faces().iter().enumerate() { - let id = FaceId(i as u64); - faces.push(None); - families - .entry(info.family.to_lowercase()) - .and_modify(|vec| vec.push(id)) - .or_insert_with(|| vec![id]); - } - - Self { - loader, - faces, - families, - buffers: HashMap::new(), - on_load: None, - } - } - - /// Query for and load the font face from the given `family` that most - /// closely matches the given `variant`. - pub fn select(&mut self, family: &str, variant: FontVariant) -> Option { - // Check whether a family with this name exists. - let ids = self.families.get(family)?; - let infos = self.loader.faces(); - - let mut best = None; - let mut best_key = None; - - // Find the best matching variant of this font. - for &id in ids { - let current = infos[id.0 as usize].variant; - - // This is a perfect match, no need to search further. - if current == variant { - best = Some(id); - break; - } - - // If this is not a perfect match, we compute a key that we want to - // minimize among all variants. This key prioritizes style, then - // stretch distance and then weight distance. - let key = ( - current.style != variant.style, - current.stretch.distance(variant.stretch), - current.weight.distance(variant.weight), - ); - - if best_key.map_or(true, |b| key < b) { - best = Some(id); - best_key = Some(key); - } - } - - // Load the face if it's not already loaded. - let id = best?; - let idx = id.0 as usize; - let slot = &mut self.faces[idx]; - if slot.is_none() { - let FaceInfo { file, index, .. } = infos[idx]; - - // Check the buffer cache since multiple faces may - // refer to the same data (font collection). - let buffer = match self.buffers.entry(file) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - let buffer = self.loader.load_file(file).ok()?; - entry.insert(Rc::new(buffer)) - } - }; - - let face = Face::new(Rc::clone(buffer), index)?; - if let Some(callback) = &self.on_load { - callback(id, &face); - } - - *slot = Some(face); - } - - best - } - - /// Get a reference to a loaded face. - /// - /// This panics if no face with this id was loaded. This function should - /// only be called with ids returned by [`select()`](Self::select). - #[track_caller] - pub fn get(&self, id: FaceId) -> &Face { - self.faces[id.0 as usize].as_ref().expect("font face was not loaded") - } - - /// Register a callback which is invoked each time a font face is loaded. - pub fn on_load(&mut self, f: F) - where - F: Fn(FaceId, &Face) + 'static, - { - self.on_load = Some(Box::new(f)); - } -} - -/// A unique identifier for a loaded font face. +/// Identifies a vertical metric of a font. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] -pub struct FaceId(u64); - -impl FaceId { - /// Create a face id from the raw underlying value. +pub enum VerticalFontMetric { + /// The distance from the baseline to the typographic ascender. /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub const fn from_raw(v: u64) -> Self { - Self(v) - } + /// Corresponds to the typographic ascender from the `OS/2` table if present + /// and falls back to the ascender from the `hhea` table otherwise. + Ascender, + /// The approximate height of uppercase letters. + CapHeight, + /// The approximate height of non-ascending lowercase letters. + XHeight, + /// The baseline on which the letters rest. + Baseline, + /// The distance from the baseline to the typographic descender. + /// + /// Corresponds to the typographic descender from the `OS/2` table if + /// present and falls back to the descender from the `hhea` table otherwise. + Descender, +} - /// Convert into the raw underlying value. - pub const fn into_raw(self) -> u64 { - self.0 +impl Display for VerticalFontMetric { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Ascender => "ascender", + Self::CapHeight => "cap-height", + Self::XHeight => "x-height", + Self::Baseline => "baseline", + Self::Descender => "descender", + }) } } @@ -358,8 +364,8 @@ impl Display for FontFamily { /// Properties of a single font face. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct FaceInfo { - /// The font file. - pub file: FileId, + /// The path to the font file. + pub path: PathBuf, /// The collection index in the font file. pub index: u32, /// The typographic font family this face is part of. diff --git a/src/image.rs b/src/image.rs index f041fac13..f98c7b1b1 100644 --- a/src/image.rs +++ b/src/image.rs @@ -2,14 +2,91 @@ use std::collections::{hash_map::Entry, HashMap}; use std::fmt::{self, Debug, Formatter}; -use std::io::Cursor; +use std::io; +use std::path::Path; use std::rc::Rc; use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; use serde::{Deserialize, Serialize}; -use crate::loading::{FileId, Loader}; +use crate::loading::{FileHash, Loader}; + +/// A unique identifier for a loaded image. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +pub struct ImageId(u32); + +impl ImageId { + /// Create an image id from the raw underlying value. + /// + /// This should only be called with values returned by + /// [`into_raw`](Self::into_raw). + pub const fn from_raw(v: u32) -> Self { + Self(v) + } + + /// Convert into the raw underlying value. + pub const fn into_raw(self) -> u32 { + self.0 + } +} + +/// Storage for loaded and decoded images. +pub struct ImageStore { + loader: Rc, + files: HashMap, + images: Vec, + on_load: Option>, +} + +impl ImageStore { + /// Create a new, empty image store. + pub fn new(loader: Rc) -> Self { + Self { + loader, + files: HashMap::new(), + images: vec![], + on_load: None, + } + } + + /// Register a callback which is invoked each time an image is loaded. + pub fn on_load(&mut self, f: F) + where + F: Fn(ImageId, &Image) + 'static, + { + self.on_load = Some(Box::new(f)); + } + + /// Load and decode an image file from a path. + pub fn load(&mut self, path: &Path) -> io::Result { + let hash = self.loader.resolve(path)?; + Ok(*match self.files.entry(hash) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let buffer = self.loader.load(path)?; + let image = Image::parse(&buffer)?; + let id = ImageId(self.images.len() as u32); + if let Some(callback) = &self.on_load { + callback(id, &image); + } + self.images.push(image); + entry.insert(id) + } + }) + } + + /// Get a reference to a loaded image. + /// + /// This panics if no image with this id was loaded. This function should + /// only be called with ids returned by this store's [`load()`](Self::load) + /// method. + #[track_caller] + pub fn get(&self, id: ImageId) -> &Image { + &self.images[id.0 as usize] + } +} /// A loaded image. pub struct Image { @@ -23,12 +100,19 @@ impl Image { /// Parse an image from raw data in a supported format (PNG or JPEG). /// /// The image format is determined automatically. - pub fn parse(data: &[u8]) -> Option { - let cursor = Cursor::new(data); - let reader = ImageReader::new(cursor).with_guessed_format().ok()?; - let format = reader.format()?; - let buf = reader.decode().ok()?; - Some(Self { format, buf }) + pub fn parse(data: &[u8]) -> io::Result { + let cursor = io::Cursor::new(data); + let reader = ImageReader::new(cursor).with_guessed_format()?; + + let format = reader.format().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "unknown image format") + })?; + + let buf = reader + .decode() + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?; + + Ok(Self { format, buf }) } /// The width of the image. @@ -52,72 +136,3 @@ impl Debug for Image { .finish() } } - -/// Caches decoded images. -pub struct ImageCache { - loader: Rc, - images: HashMap, - on_load: Option>, -} - -impl ImageCache { - /// Create a new, empty image cache. - pub fn new(loader: Rc) -> Self { - Self { - loader, - images: HashMap::new(), - on_load: None, - } - } - - /// Load and decode an image file from a path. - pub fn load(&mut self, file: FileId) -> Option { - let id = ImageId(file.into_raw()); - if let Entry::Vacant(entry) = self.images.entry(id) { - let buffer = self.loader.load_file(file).ok()?; - let image = Image::parse(&buffer)?; - if let Some(callback) = &self.on_load { - callback(id, &image); - } - entry.insert(image); - } - Some(id) - } - - /// Get a reference to a loaded image. - /// - /// This panics if no image with this id was loaded. This function should - /// only be called with ids returned by [`load()`](Self::load). - #[track_caller] - pub fn get(&self, id: ImageId) -> &Image { - &self.images[&id] - } - - /// Register a callback which is invoked each time an image is loaded. - pub fn on_load(&mut self, f: F) - where - F: Fn(ImageId, &Image) + 'static, - { - self.on_load = Some(Box::new(f)); - } -} - -/// A unique identifier for a loaded image. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] -pub struct ImageId(u64); - -impl ImageId { - /// Create an image id from the raw underlying value. - /// - /// This should only be called with values returned by - /// [`into_raw`](Self::into_raw). - pub const fn from_raw(v: u64) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub const fn into_raw(self) -> u64 { - self.0 - } -} diff --git a/src/layout/incremental.rs b/src/layout/incremental.rs index 32353d6f8..baf2991a0 100644 --- a/src/layout/incremental.rs +++ b/src/layout/incremental.rs @@ -1,5 +1,5 @@ #[cfg(feature = "layout-cache")] -use std::collections::{hash_map::Entry, HashMap}; +use std::collections::HashMap; use std::ops::Deref; use super::*; @@ -68,13 +68,10 @@ impl LayoutCache { frames: Vec>>, level: usize, ) { - let entry = FramesEntry::new(frames, level); - match self.frames.entry(hash) { - Entry::Occupied(occupied) => occupied.into_mut().push(entry), - Entry::Vacant(vacant) => { - vacant.insert(vec![entry]); - } - } + self.frames + .entry(hash) + .or_default() + .push(FramesEntry::new(frames, level)); } /// Clear the cache. diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 56e0687a0..246db7143 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -29,9 +29,9 @@ use std::hash::Hash; use std::hash::Hasher; use std::rc::Rc; -use crate::font::FontCache; +use crate::font::FontStore; use crate::geom::*; -use crate::image::ImageCache; +use crate::image::ImageStore; use crate::util::OptionExt; use crate::Context; @@ -53,11 +53,11 @@ pub trait Layout { /// The context for layouting. pub struct LayoutContext<'a> { - /// The cache for parsed font faces. - pub fonts: &'a mut FontCache, - /// The cache for decoded imges. - pub images: &'a mut ImageCache, - /// The cache for layouting artifacts. + /// Stores parsed font faces. + pub fonts: &'a mut FontStore, + /// Stores decoded images. + pub images: &'a mut ImageStore, + /// Caches layouting artifacts. #[cfg(feature = "layout-cache")] pub layouts: &'a mut LayoutCache, /// How deeply nested the current layout tree position is. diff --git a/src/lib.rs b/src/lib.rs index 0f556989f..7447dad74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,26 +53,26 @@ use std::rc::Rc; use crate::diag::TypResult; use crate::eval::{ModuleCache, Scope}; use crate::exec::State; -use crate::font::FontCache; -use crate::image::ImageCache; +use crate::font::FontStore; +use crate::image::ImageStore; use crate::layout::Frame; #[cfg(feature = "layout-cache")] use crate::layout::LayoutCache; use crate::loading::Loader; -use crate::source::{SourceFile, SourceMap}; +use crate::source::{SourceId, SourceStore}; /// The core context which holds the loader, configuration and cached artifacts. pub struct Context { /// The loader the context was created with. pub loader: Rc, /// Stores loaded source files. - pub sources: SourceMap, + pub sources: SourceStore, + /// Stores parsed font faces. + pub fonts: FontStore, + /// Stores decoded images. + pub images: ImageStore, /// Caches evaluated modules. pub modules: ModuleCache, - /// Caches parsed font faces. - pub fonts: FontCache, - /// Caches decoded images. - pub images: ImageCache, /// Caches layouting artifacts. #[cfg(feature = "layout-cache")] pub layouts: LayoutCache, @@ -93,24 +93,25 @@ impl Context { ContextBuilder::default() } - /// Garbage-collect caches. - pub fn turnaround(&mut self) { - #[cfg(feature = "layout-cache")] - self.layouts.turnaround(); - } - /// Typeset a source file into a collection of layouted frames. /// /// Returns either a vector of frames representing individual pages or /// diagnostics in the form of a vector of error message with file and span /// information. - pub fn typeset(&mut self, source: &SourceFile) -> TypResult>> { + pub fn typeset(&mut self, id: SourceId) -> TypResult>> { + let source = self.sources.get(id); let ast = parse::parse(source)?; - let module = eval::eval(self, source.file(), Rc::new(ast))?; + let module = eval::eval(self, id, Rc::new(ast))?; let tree = exec::exec(self, &module.template); let frames = layout::layout(self, &tree); Ok(frames) } + + /// Garbage-collect caches. + pub fn turnaround(&mut self) { + #[cfg(feature = "layout-cache")] + self.layouts.turnaround(); + } } /// A builder for a [`Context`]. @@ -140,10 +141,10 @@ impl ContextBuilder { /// fonts, images, source files and other resources. pub fn build(self, loader: Rc) -> Context { Context { - loader: Rc::clone(&loader), - sources: SourceMap::new(), - fonts: FontCache::new(Rc::clone(&loader)), - images: ImageCache::new(loader), + sources: SourceStore::new(Rc::clone(&loader)), + fonts: FontStore::new(Rc::clone(&loader)), + images: ImageStore::new(Rc::clone(&loader)), + loader, modules: ModuleCache::new(), #[cfg(feature = "layout-cache")] layouts: LayoutCache::new(), diff --git a/src/library/elements.rs b/src/library/elements.rs index 3d318d362..e021c0c63 100644 --- a/src/library/elements.rs +++ b/src/library/elements.rs @@ -1,8 +1,10 @@ use std::f64::consts::SQRT_2; +use std::io; use decorum::N64; use super::*; +use crate::diag::Error; use crate::layout::{ BackgroundNode, BackgroundShape, FixedNode, ImageNode, PadNode, Paint, }; @@ -13,13 +15,17 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> TypResult { let width = args.named("width")?; let height = args.named("height")?; - let file = ctx.resolve(&path.v, path.span)?; - let node = match ctx.images.load(file) { - Some(id) => ImageNode { id, width, height }, - None => bail!(args.file, path.span, "failed to load image"), - }; + let full = ctx.relpath(path.v.as_str()); + let id = ctx.images.load(&full).map_err(|err| { + Error::boxed(args.source, path.span, match err.kind() { + io::ErrorKind::NotFound => "file not found".into(), + _ => format!("failed to load image ({})", err), + }) + })?; - Ok(Value::template(move |ctx| ctx.push_into_par(node))) + Ok(Value::template(move |ctx| { + ctx.push_into_par(ImageNode { id, width, height }) + })) } /// `rect`: A rectangle with optional content. diff --git a/src/library/layout.rs b/src/library/layout.rs index 727bbcc39..0d7782069 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -6,7 +6,7 @@ use crate::paper::{Paper, PaperClass}; pub fn page(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult { let paper = match args.eat::>() { Some(name) => match Paper::from_name(&name.v) { - None => bail!(args.file, name.span, "invalid paper name"), + None => bail!(args.source, name.span, "invalid paper name"), paper => paper, }, None => None, diff --git a/src/library/text.rs b/src/library/text.rs index 5973a7e2c..cd97691c4 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -132,7 +132,7 @@ pub fn lang(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult { if dir.v.axis() == SpecAxis::Horizontal { Some(dir.v) } else { - bail!(args.file, dir.span, "must be horizontal"); + bail!(args.source, dir.span, "must be horizontal"); } } else { iso.as_deref().map(lang_dir) diff --git a/src/library/utility.rs b/src/library/utility.rs index fb39fce3d..3c157ea1c 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -25,7 +25,7 @@ pub fn len(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult { Value::Str(v) => Value::Int(v.len() as i64), Value::Array(v) => Value::Int(v.len() as i64), Value::Dict(v) => Value::Int(v.len() as i64), - _ => bail!(args.file, span, "expected string, array or dictionary"), + _ => bail!(args.source, span, "expected string, array or dictionary"), }) } @@ -35,7 +35,7 @@ pub fn rgb(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult { if let Some(string) = args.eat::>() { match RgbaColor::from_str(&string.v) { Ok(color) => color, - Err(_) => bail!(args.file, string.span, "invalid color"), + Err(_) => bail!(args.source, string.span, "invalid color"), } } else { let r = args.expect("red component")?; @@ -60,7 +60,7 @@ pub fn max(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult { /// Find the minimum or maximum of a sequence of values. fn minmax(args: &mut FuncArgs, goal: Ordering) -> TypResult { - let &mut FuncArgs { file, span, .. } = args; + let &mut FuncArgs { source, span, .. } = args; let mut extremum = args.expect::("value")?; for value in args.all::() { @@ -71,7 +71,7 @@ fn minmax(args: &mut FuncArgs, goal: Ordering) -> TypResult { } } None => bail!( - file, + source, span, "cannot compare {} with {}", extremum.type_name(), diff --git a/src/loading/fs.rs b/src/loading/fs.rs index c3ca332ee..9289519cc 100644 --- a/src/loading/fs.rs +++ b/src/loading/fs.rs @@ -1,8 +1,6 @@ -use std::cell::{Ref, RefCell}; -use std::collections::HashMap; use std::fs::{self, File}; use std::io; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::rc::Rc; use memmap2::Mmap; @@ -10,9 +8,8 @@ use same_file::Handle; use ttf_parser::{name_id, Face}; use walkdir::WalkDir; -use super::{FileId, Loader}; +use super::{FileHash, Loader}; use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight}; -use crate::util::PathExt; /// Loads fonts and images from the local file system. /// @@ -20,13 +17,12 @@ use crate::util::PathExt; #[derive(Debug, Default, Clone)] pub struct FsLoader { faces: Vec, - paths: RefCell>, } impl FsLoader { /// Create a new loader without any fonts. pub fn new() -> Self { - Self { faces: vec![], paths: RefCell::default() } + Self { faces: vec![] } } /// Builder-style variant of `search_system`. @@ -52,51 +48,6 @@ impl FsLoader { self.search_system_impl(); } - /// Search for all fonts at a path. - /// - /// If the path is a directory, all contained fonts will be searched for - /// recursively. - pub fn search_path(&mut self, dir: impl AsRef) { - let walk = WalkDir::new(dir) - .follow_links(true) - .sort_by(|a, b| a.file_name().cmp(b.file_name())) - .into_iter() - .filter_map(|e| e.ok()); - - for entry in walk { - let path = entry.path(); - if let Some(ext) = path.extension().and_then(|s| s.to_str()) { - match ext { - #[rustfmt::skip] - "ttf" | "otf" | "TTF" | "OTF" | - "ttc" | "otc" | "TTC" | "OTC" => { - self.search_file(path).ok(); - } - _ => {} - } - } - } - } - - /// Resolve a file id for a path. - pub fn resolve(&self, path: &Path) -> io::Result { - let file = File::open(path)?; - let meta = file.metadata()?; - if meta.is_file() { - let handle = Handle::from_file(file)?; - let id = FileId(fxhash::hash64(&handle)); - self.paths.borrow_mut().insert(id, path.normalize()); - Ok(id) - } else { - Err(io::Error::new(io::ErrorKind::Other, "not a file")) - } - } - - /// Return the path of a resolved file. - pub fn path(&self, id: FileId) -> Ref { - Ref::map(self.paths.borrow(), |paths| paths[&id].as_path()) - } - #[cfg(all(unix, not(target_os = "macos")))] fn search_system_impl(&mut self) { self.search_path("/usr/share/fonts"); @@ -134,6 +85,32 @@ impl FsLoader { } } + /// Search for all fonts at a path. + /// + /// If the path is a directory, all contained fonts will be searched for + /// recursively. + pub fn search_path(&mut self, dir: impl AsRef) { + let walk = WalkDir::new(dir) + .follow_links(true) + .sort_by(|a, b| a.file_name().cmp(b.file_name())) + .into_iter() + .filter_map(|e| e.ok()); + + for entry in walk { + let path = entry.path(); + if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + match ext { + #[rustfmt::skip] + "ttf" | "otf" | "TTF" | "OTF" | + "ttc" | "otc" | "TTC" | "OTC" => { + self.search_file(path).ok(); + } + _ => {} + } + } + } + } + /// Index the font faces in the file at the given path. /// /// The file may form a font collection and contain multiple font faces, @@ -180,8 +157,12 @@ impl FsLoader { stretch: FontStretch::from_number(face.width().to_number()), }; - let file = self.resolve(path)?; - self.faces.push(FaceInfo { file, index, family, variant }); + self.faces.push(FaceInfo { + path: path.to_owned(), + index, + family, + variant, + }); Ok(()) } @@ -192,16 +173,19 @@ impl Loader for FsLoader { &self.faces } - fn resolve_from(&self, base: FileId, path: &Path) -> io::Result { - let full = self.paths.borrow()[&base] - .parent() - .expect("base is a file") - .join(path); - self.resolve(&full) + fn resolve(&self, path: &Path) -> io::Result { + let file = File::open(path)?; + let meta = file.metadata()?; + if meta.is_file() { + let handle = Handle::from_file(file)?; + Ok(FileHash(fxhash::hash64(&handle))) + } else { + Err(io::Error::new(io::ErrorKind::Other, "not a file")) + } } - fn load_file(&self, id: FileId) -> io::Result> { - fs::read(&self.paths.borrow()[&id]) + fn load(&self, path: &Path) -> io::Result> { + fs::read(path) } } @@ -211,8 +195,8 @@ mod tests { #[test] fn test_index_font_dir() { - let map = FsLoader::new().with_path("fonts").paths.into_inner(); - let mut paths: Vec<_> = map.into_iter().map(|p| p.1).collect(); + let faces = FsLoader::new().with_path("fonts").faces; + let mut paths: Vec<_> = faces.into_iter().map(|info| info.path).collect(); paths.sort(); assert_eq!(paths, [ diff --git a/src/loading/mod.rs b/src/loading/mod.rs index 65eb25c6f..7d697310c 100644 --- a/src/loading/mod.rs +++ b/src/loading/mod.rs @@ -13,41 +13,24 @@ use serde::{Deserialize, Serialize}; use crate::font::FaceInfo; +/// A hash that identifies a file. +/// +/// Such a hash can be [resolved](Loader::resolve) from a path. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +pub struct FileHash(pub u64); + /// Loads resources from a local or remote source. pub trait Loader { /// Descriptions of all font faces this loader serves. fn faces(&self) -> &[FaceInfo]; - /// Resolve a `path` relative to a `base` file. - /// - /// This should return the same id for all paths pointing to the same file - /// and `None` if the file does not exist. - fn resolve_from(&self, base: FileId, path: &Path) -> io::Result; + /// Resolve a hash that is the same for this and all other paths pointing to + /// the same file. + fn resolve(&self, path: &Path) -> io::Result; - /// Load a file by id. - /// - /// This must only be called with an `id` returned by a call to this - /// loader's `resolve_from` method. - fn load_file(&self, id: FileId) -> io::Result>; -} - -/// A file id that can be [resolved](Loader::resolve_from) from a path. -/// -/// Should be the same for all paths pointing to the same file. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] -pub struct FileId(u64); - -impl FileId { - /// Create a file id from a raw value. - pub const fn from_raw(v: u64) -> Self { - Self(v) - } - - /// Convert into the raw underlying value. - pub const fn into_raw(self) -> u64 { - self.0 - } + /// Load a file from a path. + fn load(&self, path: &Path) -> io::Result>; } /// A loader which serves nothing. @@ -58,11 +41,11 @@ impl Loader for BlankLoader { &[] } - fn resolve_from(&self, _: FileId, _: &Path) -> io::Result { + fn resolve(&self, _: &Path) -> io::Result { Err(io::ErrorKind::NotFound.into()) } - fn load_file(&self, _: FileId) -> io::Result> { - panic!("resolve_from never returns an id") + fn load(&self, _: &Path) -> io::Result> { + Err(io::ErrorKind::NotFound.into()) } } diff --git a/src/main.rs b/src/main.rs index 51a6d833d..f3a97d510 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,16 @@ use std::fs; use std::io::{self, Write}; -use std::ops::Range; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process; -use anyhow::{anyhow, bail, Context}; +use anyhow::Context as _; use codespan_reporting::diagnostic::{Diagnostic, Label}; -use codespan_reporting::files::{self, Files}; use codespan_reporting::term::{self, termcolor, Config, Styles}; use same_file::is_same_file; use termcolor::{ColorChoice, StandardStream, WriteColor}; use typst::diag::{Error, Tracepoint}; -use typst::loading::{FileId, FsLoader}; -use typst::source::{SourceFile, SourceMap}; +use typst::source::SourceStore; fn main() { if let Err(error) = try_main() { @@ -32,19 +29,17 @@ fn try_main() -> anyhow::Result<()> { // Determine source and destination path. let src_path = Path::new(&args[1]); - let dest_path = if let Some(arg) = args.get(2) { - PathBuf::from(arg) - } else { - let name = src_path - .file_name() - .ok_or_else(|| anyhow!("source path is not a file"))?; - - Path::new(name).with_extension("pdf") + let dest_path = match args.get(2) { + Some(path) => path.into(), + None => { + let name = src_path.file_name().context("source path is not a file")?; + Path::new(name).with_extension("pdf") + } }; // Ensure that the source file is not overwritten. if is_same_file(src_path, &dest_path).unwrap_or(false) { - bail!("source and destination files are the same"); + anyhow::bail!("source and destination files are the same"); } // Create a loader for fonts and files. @@ -53,14 +48,15 @@ fn try_main() -> anyhow::Result<()> { .with_system() .wrap(); - // Resolve the file id of the source file and read the file. - let file = loader.resolve(src_path).context("source file not found")?; - let string = fs::read_to_string(&src_path).context("failed to read source file")?; - let source = SourceFile::new(file, string); + // Create the context which holds loaded source files, fonts, images and + // cached artifacts. + let mut ctx = typst::Context::new(loader); + + // Load the source file. + let id = ctx.sources.load(&src_path).context("source file not found")?; // Typeset. - let mut ctx = typst::Context::new(loader.clone()); - match ctx.typeset(&source) { + match ctx.typeset(id) { // Export the PDF. Ok(document) => { let buffer = typst::export::pdf(&ctx, &document); @@ -69,8 +65,7 @@ fn try_main() -> anyhow::Result<()> { // Print diagnostics. Err(errors) => { - ctx.sources.insert(source); - print_diagnostics(&loader, &ctx.sources, *errors) + print_diagnostics(&ctx.sources, *errors) .context("failed to print diagnostics")?; } } @@ -110,21 +105,19 @@ fn print_error(error: anyhow::Error) -> io::Result<()> { /// Print diagnostics messages to the terminal. fn print_diagnostics( - loader: &FsLoader, - sources: &SourceMap, + sources: &SourceStore, errors: Vec, -) -> Result<(), files::Error> { +) -> Result<(), codespan_reporting::files::Error> { let mut writer = StandardStream::stderr(ColorChoice::Always); let config = Config { tab_width: 2, ..Default::default() }; - let files = FilesImpl(loader, sources); for error in errors { // The main diagnostic. let main = Diagnostic::error() .with_message(error.message) - .with_labels(vec![Label::primary(error.file, error.span.to_range())]); + .with_labels(vec![Label::primary(error.source, error.span.to_range())]); - term::emit(&mut writer, &config, &files, &main)?; + term::emit(&mut writer, &config, sources, &main)?; // Stacktrace-like helper diagnostics. for (file, span, point) in error.trace { @@ -140,61 +133,9 @@ fn print_diagnostics( .with_message(message) .with_labels(vec![Label::primary(file, span.to_range())]); - term::emit(&mut writer, &config, &files, &help)?; + term::emit(&mut writer, &config, sources, &help)?; } } Ok(()) } - -/// Required for error message formatting with codespan-reporting. -struct FilesImpl<'a>(&'a FsLoader, &'a SourceMap); - -impl FilesImpl<'_> { - fn source(&self, id: FileId) -> Result<&SourceFile, files::Error> { - self.1.get(id).ok_or(files::Error::FileMissing) - } -} - -impl<'a> Files<'a> for FilesImpl<'a> { - type FileId = FileId; - type Name = String; - type Source = &'a str; - - fn name(&'a self, id: FileId) -> Result { - Ok(self.0.path(id).display().to_string()) - } - - fn source(&'a self, id: FileId) -> Result { - Ok(self.source(id)?.src()) - } - - fn line_index( - &'a self, - id: FileId, - byte_index: usize, - ) -> Result { - let source = self.source(id)?; - source.pos_to_line(byte_index.into()).ok_or_else(|| { - let (given, max) = (byte_index, source.len_bytes()); - if given <= max { - files::Error::InvalidCharBoundary { given } - } else { - files::Error::IndexTooLarge { given, max } - } - }) - } - - fn line_range( - &'a self, - id: FileId, - line_index: usize, - ) -> Result, files::Error> { - let source = self.source(id)?; - let span = source.line_to_span(line_index).ok_or(files::Error::LineTooLarge { - given: line_index, - max: source.len_lines(), - })?; - Ok(span.to_range()) - } -} diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 6b4787801..326fc280d 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -82,7 +82,7 @@ impl<'s> Parser<'s> { /// Add an error with location and message. pub fn error(&mut self, span: impl Into, message: impl Into) { - self.errors.push(Error::new(self.source.file(), span, message)); + self.errors.push(Error::new(self.source.id(), span, message)); } /// Eat the next token and add an error that it is not the expected `thing`. diff --git a/src/pretty.rs b/src/pretty.rs index 2f3a6ef9f..ceee61f81 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -608,7 +608,6 @@ pretty_display! { #[cfg(test)] mod tests { use super::*; - use crate::loading::FileId; use crate::parse::parse; use crate::source::SourceFile; @@ -619,7 +618,7 @@ mod tests { #[track_caller] fn test_parse(src: &str, exp: &str) { - let source = SourceFile::new(FileId::from_raw(0), src.into()); + let source = SourceFile::detached(src); let ast = parse(&source).unwrap(); let found = pretty(&ast); if exp != found { diff --git a/src/source.rs b/src/source.rs index abd3c2460..20ba137f2 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,55 +1,126 @@ //! Source files. -use std::collections::{hash_map::Entry, HashMap}; +use std::collections::HashMap; +use std::io; +use std::path::{Path, PathBuf}; +use std::rc::Rc; -use crate::loading::FileId; +#[cfg(feature = "codespan-reporting")] +use codespan_reporting::files::{self, Files}; +use serde::{Deserialize, Serialize}; + +use crate::loading::{FileHash, Loader}; use crate::parse::{is_newline, Scanner}; use crate::syntax::{Pos, Span}; +use crate::util::PathExt; -/// A store for loaded source files. -#[derive(Default)] -pub struct SourceMap { - sources: HashMap, +/// A unique identifier for a loaded source file. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Serialize, Deserialize)] +pub struct SourceId(u32); + +impl SourceId { + /// Create a source id from the raw underlying value. + /// + /// This should only be called with values returned by + /// [`into_raw`](Self::into_raw). + pub const fn from_raw(v: u32) -> Self { + Self(v) + } + + /// Convert into the raw underlying value. + pub const fn into_raw(self) -> u32 { + self.0 + } } -impl SourceMap { - /// Create a new, empty source map - pub fn new() -> Self { - Self::default() - } +/// Storage for loaded source files. +pub struct SourceStore { + loader: Rc, + files: HashMap, + sources: Vec, +} - /// Get a source file by id. - pub fn get(&self, file: FileId) -> Option<&SourceFile> { - self.sources.get(&file) - } - - /// Insert a sources. - pub fn insert(&mut self, source: SourceFile) -> &SourceFile { - match self.sources.entry(source.file) { - Entry::Occupied(mut entry) => { - entry.insert(source); - entry.into_mut() - } - Entry::Vacant(entry) => entry.insert(source), +impl SourceStore { + /// Create a new, empty source store. + pub fn new(loader: Rc) -> Self { + Self { + loader, + files: HashMap::new(), + sources: vec![], } } - /// Remove all sources. - pub fn clear(&mut self) { - self.sources.clear(); + /// Load a source file from a path using the `loader`. + pub fn load(&mut self, path: &Path) -> io::Result { + let hash = self.loader.resolve(path)?; + if let Some(&id) = self.files.get(&hash) { + return Ok(id); + } + + let data = self.loader.load(path)?; + let src = String::from_utf8(data).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8") + })?; + + Ok(self.insert(Some(hash), path, src)) + } + + /// Directly provide a source file. + /// + /// The `path` does not need to be [resolvable](Loader::resolve) through the + /// `loader`. If it is though, imports that resolve to the same file hash + /// will use the inserted file instead of going through [`Loader::load`]. + /// + /// If the path is resolvable and points to an existing source file, it is + /// overwritten. + pub fn provide(&mut self, path: &Path, src: String) -> SourceId { + if let Ok(hash) = self.loader.resolve(path) { + if let Some(&id) = self.files.get(&hash) { + // Already loaded, so we replace it. + self.sources[id.0 as usize] = SourceFile::new(id, path, src); + id + } else { + // Not loaded yet. + self.insert(Some(hash), path, src) + } + } else { + // Not known to the loader. + self.insert(None, path, src) + } + } + + /// Insert a new source file. + fn insert(&mut self, hash: Option, path: &Path, src: String) -> SourceId { + let id = SourceId(self.sources.len() as u32); + if let Some(hash) = hash { + self.files.insert(hash, id); + } + self.sources.push(SourceFile::new(id, path, src)); + id + } + + /// Get a reference to a loaded source file. + /// + /// This panics if no source file with this id was loaded. This function + /// should only be called with ids returned by this store's + /// [`load()`](Self::load) and [`provide()`](Self::provide) methods. + #[track_caller] + pub fn get(&self, id: SourceId) -> &SourceFile { + &self.sources[id.0 as usize] } } /// A single source file. pub struct SourceFile { - file: FileId, + id: SourceId, + path: PathBuf, src: String, line_starts: Vec, } impl SourceFile { - /// Create a new source file from string. - pub fn new(file: FileId, src: String) -> Self { + fn new(id: SourceId, path: &Path, src: String) -> Self { let mut line_starts = vec![Pos::ZERO]; let mut s = Scanner::new(&src); @@ -62,12 +133,27 @@ impl SourceFile { } } - Self { file, src, line_starts } + Self { + id, + path: path.normalize(), + src, + line_starts, + } } - /// The file id. - pub fn file(&self) -> FileId { - self.file + /// Create a source file without a real id and path, usually for testing. + pub fn detached(src: impl Into) -> Self { + Self::new(SourceId(0), Path::new(""), src.into()) + } + + /// The id of the source file. + pub fn id(&self) -> SourceId { + self.id + } + + /// The path to the source file. + pub fn path(&self) -> &Path { + &self.path } /// The whole source as a string slice. @@ -150,22 +236,73 @@ fn width(c: char) -> usize { if c == '\t' { 2 } else { 1 } } +impl AsRef for SourceFile { + fn as_ref(&self) -> &str { + &self.src + } +} + +#[cfg(feature = "codespan-reporting")] +impl<'a> Files<'a> for SourceStore { + type FileId = SourceId; + type Name = std::path::Display<'a>; + type Source = &'a SourceFile; + + fn name(&'a self, id: SourceId) -> Result { + Ok(self.get(id).path().display()) + } + + fn source(&'a self, id: SourceId) -> Result { + Ok(self.get(id)) + } + + fn line_index( + &'a self, + id: SourceId, + byte_index: usize, + ) -> Result { + let source = self.get(id); + source.pos_to_line(byte_index.into()).ok_or_else(|| { + let (given, max) = (byte_index, source.len_bytes()); + if given <= max { + files::Error::InvalidCharBoundary { given } + } else { + files::Error::IndexTooLarge { given, max } + } + }) + } + + fn line_range( + &'a self, + id: SourceId, + line_index: usize, + ) -> Result, files::Error> { + let source = self.get(id); + match source.line_to_span(line_index) { + Some(span) => Ok(span.to_range()), + None => Err(files::Error::LineTooLarge { + given: line_index, + max: source.len_lines(), + }), + } + } +} + #[cfg(test)] mod tests { use super::*; - const ID: FileId = FileId::from_raw(0); const TEST: &str = "äbcde\nf💛g\r\nhi\rjkl"; #[test] fn test_source_file_new() { - let source = SourceFile::new(ID, TEST.into()); + let source = SourceFile::detached(TEST); assert_eq!(source.line_starts, vec![Pos(0), Pos(7), Pos(15), Pos(18)]); } #[test] fn test_source_file_pos_to_line() { - let source = SourceFile::new(ID, TEST.into()); + let source = SourceFile::detached(TEST); assert_eq!(source.pos_to_line(Pos(0)), Some(0)); assert_eq!(source.pos_to_line(Pos(2)), Some(0)); assert_eq!(source.pos_to_line(Pos(6)), Some(0)); @@ -186,7 +323,7 @@ mod tests { assert_eq!(result, byte_pos); } - let source = SourceFile::new(ID, TEST.into()); + let source = SourceFile::detached(TEST); roundtrip(&source, Pos(0)); roundtrip(&source, Pos(7)); roundtrip(&source, Pos(12)); diff --git a/tests/typ/code/import.typ b/tests/typ/code/import.typ index 26a3404c7..953b522e1 100644 --- a/tests/typ/code/import.typ +++ b/tests/typ/code/import.typ @@ -49,7 +49,7 @@ --- // Some non-text stuff. -// Error: 16-37 file is not valid utf-8 +// Error: 16-37 failed to load source file (file is not valid utf-8) #import * from "../../res/rhino.png" --- diff --git a/tests/typ/insert/image.typ b/tests/typ/insert/image.typ index 840699495..9d9c6d46a 100644 --- a/tests/typ/insert/image.typ +++ b/tests/typ/insert/image.typ @@ -36,5 +36,5 @@ #image("path/does/not/exist") --- -// Error: 8-21 failed to load image +// Error: 8-21 failed to load image (unknown image format) #image("./image.typ") diff --git a/tests/typeset.rs b/tests/typeset.rs index 78694206b..a08ece2d0 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -16,9 +16,9 @@ use typst::exec::{exec, State}; use typst::geom::{self, Length, PathElement, Point, Sides, Size}; use typst::image::ImageId; use typst::layout::{layout, Element, Frame, Geometry, LayoutTree, Paint, Text}; -use typst::loading::{FileId, FsLoader}; +use typst::loading::FsLoader; use typst::parse::{parse, Scanner}; -use typst::source::SourceFile; +use typst::source::{SourceFile, SourceId}; use typst::syntax::Pos; use typst::Context; @@ -71,7 +71,7 @@ fn main() { let rhs = args.expect::("right-hand side")?; if lhs != rhs { typst::bail!( - args.file, + args.source, args.span, "Assertion failed: {:?} != {:?}", lhs, @@ -83,7 +83,7 @@ fn main() { // Create loader and context. let loader = FsLoader::new().with_path(FONT_DIR).wrap(); - let mut ctx = Context::builder().std(std).state(state).build(loader.clone()); + let mut ctx = Context::builder().std(std).state(state).build(loader); // Run all the tests. let mut ok = true; @@ -96,7 +96,6 @@ fn main() { ok &= test( &mut ctx, - loader.as_ref(), &src_path, &png_path, &ref_path, @@ -144,7 +143,6 @@ impl Args { fn test( ctx: &mut Context, - loader: &FsLoader, src_path: &Path, png_path: &Path, ref_path: &Path, @@ -153,7 +151,6 @@ fn test( let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path); println!("Testing {}", name.display()); - let file = loader.resolve(src_path).unwrap(); let src = fs::read_to_string(src_path).unwrap(); let mut ok = true; @@ -178,7 +175,7 @@ fn test( } } else { let (part_ok, compare_here, part_frames) = - test_part(ctx, file, part, i, compare_ref, line); + test_part(ctx, src_path, part.into(), i, compare_ref, line); ok &= part_ok; compare_ever |= compare_here; frames.extend(part_frames); @@ -218,19 +215,21 @@ fn test( fn test_part( ctx: &mut Context, - file: FileId, - src: &str, + src_path: &Path, + src: String, i: usize, compare_ref: bool, line: usize, ) -> (bool, bool, Vec>) { - let source = SourceFile::new(file, src.into()); + let id = ctx.sources.provide(src_path, src); + let source = ctx.sources.get(id); + let (local_compare_ref, mut ref_errors) = parse_metadata(&source); let compare_ref = local_compare_ref.unwrap_or(compare_ref); let mut ok = true; - let result = typeset(ctx, &source); + let result = typeset(ctx, id); let (frames, mut errors) = match result { #[allow(unused_variables)] Ok((tree, mut frames)) => { @@ -247,7 +246,7 @@ fn test_part( }; // TODO: Also handle errors from other files. - errors.retain(|error| error.file == source.file()); + errors.retain(|error| error.source == id); for error in &mut errors { error.trace.clear(); } @@ -259,8 +258,9 @@ fn test_part( println!(" Subtest {} does not match expected errors. ❌", i); ok = false; + let source = ctx.sources.get(id); for error in errors.iter() { - if error.file == file && !ref_errors.contains(error) { + if error.source == id && !ref_errors.contains(error) { print!(" Not annotated | "); print_error(&source, line, error); } @@ -277,6 +277,15 @@ fn test_part( (ok, compare_ref, frames) } +fn typeset(ctx: &mut Context, id: SourceId) -> TypResult<(LayoutTree, Vec>)> { + let source = ctx.sources.get(id); + let ast = parse(source)?; + let module = eval(ctx, id, Rc::new(ast))?; + let tree = exec(ctx, &module.template); + let frames = layout(ctx, &tree); + Ok((tree, frames)) +} + #[cfg(feature = "layout-cache")] fn test_incremental( ctx: &mut Context, @@ -362,28 +371,17 @@ fn parse_metadata(source: &SourceFile) -> (Option, Vec) { let start = pos(&mut s); let end = if s.eat_if('-') { pos(&mut s) } else { start }; - errors.push(Error::new(source.file(), start .. end, s.rest().trim())); + errors.push(Error::new(source.id(), start .. end, s.rest().trim())); } (compare_ref, errors) } -fn typeset( - ctx: &mut Context, - source: &SourceFile, -) -> TypResult<(LayoutTree, Vec>)> { - let ast = parse(source)?; - let module = eval(ctx, source.file(), Rc::new(ast))?; - let tree = exec(ctx, &module.template); - let frames = layout(ctx, &tree); - Ok((tree, frames)) -} - fn print_error(source: &SourceFile, line: usize, error: &Error) { - let start_line = line + source.pos_to_line(error.span.start).unwrap(); - let start_col = source.pos_to_column(error.span.start).unwrap(); - let end_line = line + source.pos_to_line(error.span.end).unwrap(); - let end_col = source.pos_to_column(error.span.end).unwrap(); + let start_line = 1 + line + source.pos_to_line(error.span.start).unwrap(); + let start_col = 1 + source.pos_to_column(error.span.start).unwrap(); + let end_line = 1 + line + source.pos_to_line(error.span.end).unwrap(); + let end_col = 1 + source.pos_to_column(error.span.end).unwrap(); println!( "Error: {}:{}-{}:{}: {}", start_line, start_col, end_line, end_col, error.message