New source loading architecture

This commit is contained in:
Laurenz 2021-08-09 11:06:37 +02:00
parent 3c92bad9a7
commit 3932bb2cb9
24 changed files with 738 additions and 672 deletions

View File

@ -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<RefCell<Context>>,
source: SourceFile,
id: SourceId,
ast: Rc<SyntaxTree>,
module: Module,
tree: LayoutTree,
@ -90,26 +88,23 @@ struct Case {
}
impl Case {
fn new(ctx: Rc<RefCell<Context>>, source: SourceFile) -> Self {
fn new(ctx: Rc<RefCell<Context>>, 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<Module> {
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<Vec<Rc<Frame>>> {
self.ctx.borrow_mut().typeset(&self.source)
self.ctx.borrow_mut().typeset(self.id)
}
fn pdf(&self) -> Vec<u8> {

View File

@ -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<SyntaxTree> {
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);

View File

@ -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<T> = Result<T, String>;
/// 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<Span>, message: impl Into<String>) -> Self {
pub fn new(
source: SourceId,
span: impl Into<Span>,
message: impl Into<String>,
) -> 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<Span>,
message: impl Into<String>,
) -> Box<Vec<Self>> {
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<Span>,
) -> impl FnOnce(String) -> Box<Vec<Self>> {
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),+));
};
}

View File

@ -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(())
}

View File

@ -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<SyntaxTree>) -> TypResult<Module> {
let mut ctx = EvalContext::new(ctx, file);
pub fn eval(
ctx: &mut Context,
source: SourceId,
ast: Rc<SyntaxTree>,
) -> TypResult<Module> {
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<FileId, Module>;
pub type ModuleCache = HashMap<SourceId, Module>;
/// 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<FileId>,
pub route: Vec<SourceId>,
/// 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<FileId> {
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<FileId> {
let file = self.resolve(path, span)?;
pub fn import(&mut self, path: &str, span: Span) -> TypResult<SourceId> {
// 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<Path>) -> 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::<Function>()
.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<Self::Output> {
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<Self::Output> {
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::<Function>()
.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::<bool>()
.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::<bool>()
.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::<EcoString>()
.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::<EcoString>()
.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];

View File

@ -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<Frame>]) -> Vec<u8> {
@ -33,19 +33,16 @@ pub fn pdf(ctx: &Context, frames: &[Rc<Frame>]) -> Vec<u8> {
struct PdfExporter<'a> {
writer: PdfWriter,
frames: &'a [Rc<Frame>],
fonts: &'a FontCache,
font_map: Remapper<FaceId>,
images: &'a ImageCache,
image_map: Remapper<ImageId>,
refs: Refs,
frames: &'a [Rc<Frame>],
fonts: &'a FontStore,
images: &'a ImageStore,
font_map: Remapper<FaceId>,
image_map: Remapper<ImageId>,
}
impl<'a> PdfExporter<'a> {
fn new(ctx: &'a Context, frames: &'a [Rc<Frame>]) -> 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,
}

View File

@ -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<dyn Loader>,
faces: Vec<Option<Face>>,
families: HashMap<String, Vec<FaceId>>,
buffers: HashMap<FileHash, Rc<Vec<u8>>>,
on_load: Option<Box<dyn Fn(FaceId, &Face)>>,
}
impl FontStore {
/// Create a new, empty font store.
pub fn new(loader: Rc<dyn Loader>) -> Self {
let mut faces = vec![];
let mut families = HashMap::<String, Vec<FaceId>>::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<F>(&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<FaceId> {
// 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<dyn Loader>,
faces: Vec<Option<Face>>,
families: HashMap<String, Vec<FaceId>>,
buffers: HashMap<FileId, Rc<Vec<u8>>>,
on_load: Option<Box<dyn Fn(FaceId, &Face)>>,
}
impl FontCache {
/// Create a new, empty font cache.
pub fn new(loader: Rc<dyn Loader>) -> Self {
let mut faces = vec![];
let mut families = HashMap::<String, Vec<FaceId>>::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<FaceId> {
// 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<F>(&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.

View File

@ -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<dyn Loader>,
files: HashMap<FileHash, ImageId>,
images: Vec<Image>,
on_load: Option<Box<dyn Fn(ImageId, &Image)>>,
}
impl ImageStore {
/// Create a new, empty image store.
pub fn new(loader: Rc<dyn Loader>) -> 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<F>(&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<ImageId> {
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<Self> {
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<Self> {
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<dyn Loader>,
images: HashMap<ImageId, Image>,
on_load: Option<Box<dyn Fn(ImageId, &Image)>>,
}
impl ImageCache {
/// Create a new, empty image cache.
pub fn new(loader: Rc<dyn Loader>) -> 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<ImageId> {
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<F>(&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
}
}

View File

@ -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<Constrained<Rc<Frame>>>,
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.

View File

@ -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.

View File

@ -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<dyn Loader>,
/// 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<Vec<Rc<Frame>>> {
pub fn typeset(&mut self, id: SourceId) -> TypResult<Vec<Rc<Frame>>> {
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<dyn Loader>) -> 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(),

View File

@ -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<Value> {
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.

View File

@ -6,7 +6,7 @@ use crate::paper::{Paper, PaperClass};
pub fn page(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
let paper = match args.eat::<Spanned<EcoString>>() {
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,

View File

@ -132,7 +132,7 @@ pub fn lang(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
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)

View File

@ -25,7 +25,7 @@ pub fn len(_: &mut EvalContext, args: &mut FuncArgs) -> TypResult<Value> {
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<Value> {
if let Some(string) = args.eat::<Spanned<EcoString>>() {
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<Value> {
/// Find the minimum or maximum of a sequence of values.
fn minmax(args: &mut FuncArgs, goal: Ordering) -> TypResult<Value> {
let &mut FuncArgs { file, span, .. } = args;
let &mut FuncArgs { source, span, .. } = args;
let mut extremum = args.expect::<Value>("value")?;
for value in args.all::<Value>() {
@ -71,7 +71,7 @@ fn minmax(args: &mut FuncArgs, goal: Ordering) -> TypResult<Value> {
}
}
None => bail!(
file,
source,
span,
"cannot compare {} with {}",
extremum.type_name(),

View File

@ -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<FaceInfo>,
paths: RefCell<HashMap<FileId, PathBuf>>,
}
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<Path>) {
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<FileId> {
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<Path> {
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<Path>) {
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<FileId> {
let full = self.paths.borrow()[&base]
.parent()
.expect("base is a file")
.join(path);
self.resolve(&full)
fn resolve(&self, path: &Path) -> io::Result<FileHash> {
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<Vec<u8>> {
fs::read(&self.paths.borrow()[&id])
fn load(&self, path: &Path) -> io::Result<Vec<u8>> {
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, [

View File

@ -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<FileId>;
/// 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<FileHash>;
/// 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<Vec<u8>>;
}
/// 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<Vec<u8>>;
}
/// A loader which serves nothing.
@ -58,11 +41,11 @@ impl Loader for BlankLoader {
&[]
}
fn resolve_from(&self, _: FileId, _: &Path) -> io::Result<FileId> {
fn resolve(&self, _: &Path) -> io::Result<FileHash> {
Err(io::ErrorKind::NotFound.into())
}
fn load_file(&self, _: FileId) -> io::Result<Vec<u8>> {
panic!("resolve_from never returns an id")
fn load(&self, _: &Path) -> io::Result<Vec<u8>> {
Err(io::ErrorKind::NotFound.into())
}
}

View File

@ -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<Error>,
) -> 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<Self::Name, files::Error> {
Ok(self.0.path(id).display().to_string())
}
fn source(&'a self, id: FileId) -> Result<Self::Source, files::Error> {
Ok(self.source(id)?.src())
}
fn line_index(
&'a self,
id: FileId,
byte_index: usize,
) -> Result<usize, files::Error> {
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<Range<usize>, 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())
}
}

View File

@ -82,7 +82,7 @@ impl<'s> Parser<'s> {
/// Add an error with location and message.
pub fn error(&mut self, span: impl Into<Span>, message: impl Into<String>) {
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`.

View File

@ -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 {

View File

@ -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<FileId, SourceFile>,
/// 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<dyn Loader>,
files: HashMap<FileHash, SourceId>,
sources: Vec<SourceFile>,
}
/// 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<dyn Loader>) -> 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<SourceId> {
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<FileHash>, 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<Pos>,
}
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<String>) -> 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<str> 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<Self::Name, files::Error> {
Ok(self.get(id).path().display())
}
fn source(&'a self, id: SourceId) -> Result<Self::Source, files::Error> {
Ok(self.get(id))
}
fn line_index(
&'a self,
id: SourceId,
byte_index: usize,
) -> Result<usize, files::Error> {
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<std::ops::Range<usize>, 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));

View File

@ -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"
---

View File

@ -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")

View File

@ -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::<Value>("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<Rc<Frame>>) {
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<Rc<Frame>>)> {
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<bool>, Vec<Error>) {
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<Rc<Frame>>)> {
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