summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-08-09 11:06:37 +0200
committerLaurenz <laurmaedje@gmail.com>2021-08-09 11:26:41 +0200
commit3932bb2cb93be95d67fc56998423eb9ce047fdfa (patch)
treec36bd4df1d2c74f8ae100d2f3bd3a0b232b797f5 /src
parent3c92bad9a7cd6b880de197806443ffcce2cac9d8 (diff)
New source loading architecture
Diffstat (limited to 'src')
-rw-r--r--src/diag.rs32
-rw-r--r--src/eval/function.rs12
-rw-r--r--src/eval/mod.rs152
-rw-r--r--src/export/pdf.rs22
-rw-r--r--src/font.rs342
-rw-r--r--src/image.rs167
-rw-r--r--src/layout/incremental.rs13
-rw-r--r--src/layout/mod.rs14
-rw-r--r--src/lib.rs41
-rw-r--r--src/library/elements.rs20
-rw-r--r--src/library/layout.rs2
-rw-r--r--src/library/text.rs2
-rw-r--r--src/library/utility.rs8
-rw-r--r--src/loading/fs.rs112
-rw-r--r--src/loading/mod.rs47
-rw-r--r--src/main.rs105
-rw-r--r--src/parse/parser.rs2
-rw-r--r--src/pretty.rs3
-rw-r--r--src/source.rs205
19 files changed, 688 insertions, 613 deletions
diff --git a/src/diag.rs b/src/diag.rs
index 397a833f..0fcdef17 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<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),+));
};
}
diff --git a/src/eval/function.rs b/src/eval/function.rs
index b9a168d2..8b1e883f 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 8f5532eb..22c7c0b4 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<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(id)
+ }
- Ok(file)
+ /// 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];
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index bfd36421..d4b3ac25 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<Frame>]) -> Vec<u8> {
@@ -33,19 +33,16 @@ pub fn pdf(ctx: &Context, frames: &[Rc<Frame>]) -> Vec<u8> {
struct PdfExporter<'a> {
writer: PdfWriter,
+ refs: Refs,
frames: &'a [Rc<Frame>],
- fonts: &'a FontCache,
+ fonts: &'a FontStore,
+ images: &'a ImageStore,
font_map: Remapper<FaceId>,
- images: &'a ImageCache,
image_map: Remapper<ImageId>,
- refs: Refs,
}
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,
}
diff --git a/src/font.rs b/src/font.rs
index a609e934..e756f84e 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<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.
diff --git a/src/image.rs b/src/image.rs
index f041fac1..f98c7b1b 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -2,122 +2,137 @@
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 loaded image.
-pub struct Image {
- /// The original format the image was encoded in.
- pub format: ImageFormat,
- /// The decoded image.
- pub buf: DynamicImage,
-}
+/// A unique identifier for a loaded image.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+#[derive(Serialize, Deserialize)]
+pub struct ImageId(u32);
-impl Image {
- /// Parse an image from raw data in a supported format (PNG or JPEG).
+impl ImageId {
+ /// Create an image id from the raw underlying value.
///
- /// 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 })
- }
-
- /// The width of the image.
- pub fn width(&self) -> u32 {
- self.buf.width()
- }
-
- /// The height of the image.
- pub fn height(&self) -> u32 {
- self.buf.height()
+ /// This should only be called with values returned by
+ /// [`into_raw`](Self::into_raw).
+ pub const fn from_raw(v: u32) -> Self {
+ Self(v)
}
-}
-impl Debug for Image {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.debug_struct("Image")
- .field("format", &self.format)
- .field("color", &self.buf.color())
- .field("width", &self.width())
- .field("height", &self.height())
- .finish()
+ /// Convert into the raw underlying value.
+ pub const fn into_raw(self) -> u32 {
+ self.0
}
}
-/// Caches decoded images.
-pub struct ImageCache {
+/// Storage for loaded and decoded images.
+pub struct ImageStore {
loader: Rc<dyn Loader>,
- images: HashMap<ImageId, Image>,
+ files: HashMap<FileHash, ImageId>,
+ images: Vec<Image>,
on_load: Option<Box<dyn Fn(ImageId, &Image)>>,
}
-impl ImageCache {
- /// Create a new, empty image cache.
+impl ImageStore {
+ /// Create a new, empty image store.
pub fn new(loader: Rc<dyn Loader>) -> Self {
Self {
loader,
- images: HashMap::new(),
+ 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, 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);
+ 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)
}
- 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).
+ /// 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]
- }
-
- /// 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));
+ &self.images[id.0 as usize]
}
}
-/// A unique identifier for a loaded image.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-#[derive(Serialize, Deserialize)]
-pub struct ImageId(u64);
+/// A loaded image.
+pub struct Image {
+ /// The original format the image was encoded in.
+ pub format: ImageFormat,
+ /// The decoded image.
+ pub buf: DynamicImage,
+}
-impl ImageId {
- /// Create an image id from the raw underlying value.
+impl Image {
+ /// Parse an image from raw data in a supported format (PNG or JPEG).
///
- /// This should only be called with values returned by
- /// [`into_raw`](Self::into_raw).
- pub const fn from_raw(v: u64) -> Self {
- Self(v)
+ /// The image format is determined automatically.
+ 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 })
}
- /// Convert into the raw underlying value.
- pub const fn into_raw(self) -> u64 {
- self.0
+ /// The width of the image.
+ pub fn width(&self) -> u32 {
+ self.buf.width()
+ }
+
+ /// The height of the image.
+ pub fn height(&self) -> u32 {
+ self.buf.height()
+ }
+}
+
+impl Debug for Image {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.debug_struct("Image")
+ .field("format", &self.format)
+ .field("color", &self.buf.color())
+ .field("width", &self.width())
+ .field("height", &self.height())
+ .finish()
}
}
diff --git a/src/layout/incremental.rs b/src/layout/incremental.rs
index 32353d6f..baf2991a 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<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.
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 56e0687a..246db714 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 0f556989..7447dad7 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<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(),
diff --git a/src/library/elements.rs b/src/library/elements.rs
index 3d318d36..e021c0c6 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<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"),
- };
-
- Ok(Value::template(move |ctx| ctx.push_into_par(node)))
+ 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(ImageNode { id, width, height })
+ }))
}
/// `rect`: A rectangle with optional content.
diff --git a/src/library/layout.rs b/src/library/layout.rs
index 727bbcc3..0d778206 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<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,
diff --git a/src/library/text.rs b/src/library/text.rs
index 5973a7e2..cd97691c 100644
--- a/src/library/text.rs
+++ b/src/library/text.rs
@@ -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)
diff --git a/src/library/utility.rs b/src/library/utility.rs
index fb39fce3..3c157ea1 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> {
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(),
diff --git a/src/loading/fs.rs b/src/loading/fs.rs
index c3ca332e..9289519c 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<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, [
diff --git a/src/loading/mod.rs b/src/loading/mod.rs
index 65eb25c6..7d697310 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<FileId>;
-
- /// 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);
+ /// 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>;
-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())
}
}
diff --git a/src/main.rs b/src/main.rs
index 51a6d833..f3a97d51 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<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())
- }
-}
diff --git a/src/parse/parser.rs b/src/parse/parser.rs
index 6b478780..326fc280 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<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`.
diff --git a/src/pretty.rs b/src/pretty.rs
index 2f3a6ef9..ceee61f8 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 abd3c246..20ba137f 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 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)
+ }
-/// A store for loaded source files.
-#[derive(Default)]
-pub struct SourceMap {
- sources: HashMap<FileId, SourceFile>,
+ /// Convert into the raw underlying value.
+ pub const fn into_raw(self) -> u32 {
+ self.0
+ }
+}
+
+/// Storage for loaded source files.
+pub struct SourceStore {
+ loader: Rc<dyn Loader>,
+ files: HashMap<FileHash, SourceId>,
+ sources: Vec<SourceFile>,
}
-impl SourceMap {
- /// Create a new, empty source map
- pub fn new() -> Self {
- Self::default()
+impl SourceStore {
+ /// Create a new, empty source store.
+ pub fn new(loader: Rc<dyn Loader>) -> Self {
+ Self {
+ loader,
+ files: HashMap::new(),
+ sources: vec![],
+ }
}
- /// Get a source file by id.
- pub fn get(&self, file: FileId) -> Option<&SourceFile> {
- self.sources.get(&file)
+ /// 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))
}
- /// 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()
+ /// 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)
}
- Entry::Vacant(entry) => entry.insert(source),
+ } 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
}
- /// Remove all sources.
- pub fn clear(&mut self) {
- self.sources.clear();
+ /// 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,
+ }
+ }
+
+ /// 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 file id.
- pub fn file(&self) -> FileId {
- self.file
+ /// 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));