refactor: apply some suggestions

- rename Loaded::bytes to data
- change `let data = source.load()` to `let loaded`
- only expose a single `within` method for the LoadedWithin trait
    - invalid utf-8 data is a rare edge case
- only store one EcoString inside LoadError
    - mutate it when using `LoadedWithin::within` method
This commit is contained in:
Tobias Schmitz 2025-06-03 16:47:07 +02:00
parent 1c08683248
commit 2d3e883d2b
No known key found for this signature in database
16 changed files with 195 additions and 215 deletions

View File

@ -190,7 +190,7 @@ impl SystemWorld {
source.lines()
} else if let Some(bytes) = slot.file.get() {
let bytes = bytes.as_ref().expect("file is not valid");
Lines::from_bytes(bytes.as_slice()).expect("file is not valid utf-8")
Lines::from_bytes(bytes).expect("file is not valid utf-8")
} else {
panic!("file id does not point to any source file");
}

View File

@ -1,6 +1,6 @@
use std::ffi::OsStr;
use typst_library::diag::{warning, At, LoadedAt, SourceResult, StrResult};
use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
use typst_library::introspection::Locator;
@ -27,17 +27,17 @@ pub fn layout_image(
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let Derived { source, derived: data } = &elem.source;
let Derived { source, derived: loaded } = &elem.source;
let format = match elem.format(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(source, &data.bytes).at(span)?,
Smart::Auto => determine_format(source, &loaded.data).at(span)?,
};
// Warn the user if the image contains a foreign object. Not perfect
// because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object =
memchr::memmem::find(&data.bytes, b"<foreignObject").is_some();
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
if has_foreign_object {
engine.sink.warn(warning!(
@ -53,7 +53,7 @@ pub fn layout_image(
let kind = match format {
ImageFormat::Raster(format) => ImageKind::Raster(
RasterImage::new(
data.bytes.clone(),
loaded.data.clone(),
format,
elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
)
@ -61,11 +61,11 @@ pub fn layout_image(
),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts(
data.bytes.clone(),
loaded.data.clone(),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.in_text(data)?,
.within(loaded)?,
),
};

View File

@ -1,6 +1,6 @@
//! Diagnostics.
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Display, Formatter, Write as _};
use std::io;
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
@ -581,9 +581,9 @@ pub type LoadResult<T> = Result<T, LoadError>;
/// [`FileId`]: typst_syntax::FileId
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LoadError {
pub pos: ReportPos,
pub message: EcoString,
pub error: EcoString,
pos: ReportPos,
/// Must contain a message formatted like this: `"failed to do thing (cause)"`.
message: EcoString,
}
impl LoadError {
@ -594,109 +594,98 @@ impl LoadError {
) -> Self {
Self {
pos: pos.into(),
message: eco_format!("{message}"),
error: eco_format!("{error}"),
message: eco_format!("{message} ({error})"),
}
}
}
/// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`] context.
pub trait LoadedAt<T> {
/// Add the span information.
fn in_text(self, data: &Loaded) -> SourceResult<T>;
/// Add the span information.
fn in_invalid_text(self, data: &Loaded) -> SourceResult<T>;
pub trait LoadedWithin<T> {
/// Report an error, possibly in an external file.
fn within(self, loaded: &Loaded) -> SourceResult<T>;
}
impl<T> LoadedAt<T> for Result<T, LoadError> {
/// Report an error, possibly in an external file.
fn in_text(self, data: &Loaded) -> SourceResult<T> {
self.map_err(|err| data.err_in_text(err.pos, err.message, err.error))
}
/// Report an error in invalid text.
fn in_invalid_text(self, data: &Loaded) -> SourceResult<T> {
self.map_err(|err| data.err_in_invalid_text(err.pos, err.message, err.error))
impl<T> LoadedWithin<T> for Result<T, LoadError> {
fn within(self, loaded: &Loaded) -> SourceResult<T> {
self.map_err(|err| load_err_in_text(loaded, err.pos, err.message))
}
}
impl Loaded {
/// Report an error, possibly in an external file.
pub fn err_in_text(
&self,
pos: impl Into<ReportPos>,
msg: impl std::fmt::Display,
error: impl std::fmt::Display,
) -> EcoVec<SourceDiagnostic> {
let pos = pos.into();
// This also does utf-8 validation. Only report an error in an external
// file if it is human readable (valid utf-8), otherwise fall back to
// `err_in_invalid_text`.
let lines = Lines::from_bytes(&self.bytes);
match (self.source.v, lines) {
(LoadSource::Path(file_id), Ok(lines)) => {
if let Some(range) = pos.range(&lines) {
let span = Span::from_range(file_id, range);
return eco_vec!(error!(span, "{msg} ({error})"));
}
/// Report an error, possibly in an external file. This will delegate to
/// [`load_err_in_invalid_text`] if the data isn't valid utf-8.
fn load_err_in_text(
loaded: &Loaded,
pos: impl Into<ReportPos>,
mut message: EcoString,
) -> EcoVec<SourceDiagnostic> {
let pos = pos.into();
// This also does utf-8 validation. Only report an error in an external
// file if it is human readable (valid utf-8), otherwise fall back to
// `load_err_in_invalid_text`.
let lines = Lines::from_bytes(&loaded.data);
match (loaded.source.v, lines) {
(LoadSource::Path(file_id), Ok(lines)) => {
if let Some(range) = pos.range(&lines) {
let span = Span::from_range(file_id, range);
return eco_vec![SourceDiagnostic::error(span, message)];
}
// Either `ReportPos::None` was provided, or resolving the range
// from the line/column failed. If present report the possibly
// wrong line/column in the error message anyway.
let span = Span::from_range(file_id, 0..self.bytes.len());
let error = if let Some(pair) = pos.line_col(&lines) {
let (line, col) = pair.numbers();
error!(span, "{msg} ({error} at {line}:{col})")
} else {
error!(span, "{msg} ({error})")
};
eco_vec![error]
// Either `ReportPos::None` was provided, or resolving the range
// from the line/column failed. If present report the possibly
// wrong line/column in the error message anyway.
let span = Span::from_range(file_id, 0..loaded.data.len());
if let Some(pair) = pos.line_col(&lines) {
message.pop();
let (line, col) = pair.numbers();
write!(&mut message, " at {line}:{col})").ok();
}
(LoadSource::Bytes, Ok(lines)) => {
let error = if let Some(pair) = pos.line_col(&lines) {
let (line, col) = pair.numbers();
error!(self.source.span, "{msg} ({error} at {line}:{col})")
} else {
error!(self.source.span, "{msg} ({error})")
};
eco_vec![error]
}
_ => self.err_in_invalid_text(pos, msg, error),
eco_vec![SourceDiagnostic::error(span, message)]
}
(LoadSource::Bytes, Ok(lines)) => {
if let Some(pair) = pos.line_col(&lines) {
message.pop();
let (line, col) = pair.numbers();
write!(&mut message, " at {line}:{col})").ok();
}
eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
}
_ => load_err_in_invalid_text(loaded, pos, message),
}
}
/// Report an error (possibly from an external file) that isn't valid utf-8.
pub fn err_in_invalid_text(
&self,
pos: impl Into<ReportPos>,
msg: impl std::fmt::Display,
error: impl std::fmt::Display,
) -> EcoVec<SourceDiagnostic> {
let line_col = pos.into().try_line_col(&self.bytes).map(|p| p.numbers());
let error = match (self.source.v, line_col) {
(LoadSource::Path(file), _) => {
let path = if let Some(package) = file.package() {
format!("{package}{}", file.vpath().as_rooted_path().display())
} else {
format!("{}", file.vpath().as_rootless_path().display())
};
if let Some((line, col)) = line_col {
error!(self.source.span, "{msg} ({error} in {path}:{line}:{col})")
} else {
error!(self.source.span, "{msg} ({error} in {path})")
}
/// Report an error (possibly from an external file) that isn't valid utf-8.
fn load_err_in_invalid_text(
loaded: &Loaded,
pos: impl Into<ReportPos>,
mut message: EcoString,
) -> EcoVec<SourceDiagnostic> {
let line_col = pos.into().try_line_col(&loaded.data).map(|p| p.numbers());
match (loaded.source.v, line_col) {
(LoadSource::Path(file), _) => {
message.pop();
if let Some(package) = file.package() {
write!(
&mut message,
" in {package}{}",
file.vpath().as_rooted_path().display()
)
.ok();
} else {
write!(&mut message, " in {}", file.vpath().as_rootless_path().display())
.ok();
};
if let Some((line, col)) = line_col {
write!(&mut message, ":{line}:{col}").ok();
}
(LoadSource::Bytes, Some((line, col))) => {
error!(self.source.span, "{msg} ({error} at {line}:{col})")
}
(LoadSource::Bytes, None) => {
error!(self.source.span, "{msg} ({error})")
}
};
eco_vec![error]
message.push(')');
}
(LoadSource::Bytes, Some((line, col))) => {
message.pop();
write!(&mut message, " at {line}:{col})").ok();
}
(LoadSource::Bytes, None) => (),
}
eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
@ -819,22 +808,21 @@ impl LineCol {
/// Format a user-facing error message for an XML-like file format.
pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError {
let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
let message = eco_format!("failed to parse {format}");
let error = match error {
let message = match error {
roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => {
eco_format!("found closing tag '{actual}' instead of '{expected}'")
eco_format!("failed to parse {format} (found closing tag '{actual}' instead of '{expected}')")
}
roxmltree::Error::UnknownEntityReference(entity, _) => {
eco_format!("unknown entity '{entity}'")
eco_format!("failed to parse {format} (unknown entity '{entity}')")
}
roxmltree::Error::DuplicatedAttribute(attr, _) => {
eco_format!("duplicate attribute '{attr}'")
eco_format!("failed to parse {format} (duplicate attribute '{attr}')")
}
roxmltree::Error::NoRootNode => {
eco_format!("missing root node")
eco_format!("failed to parse {format} (missing root node)")
}
err => eco_format!("{err}"),
err => eco_format!("failed to parse {format} ({err})"),
};
LoadError { pos: pos.into(), message, error }
LoadError { pos: pos.into(), message }
}

View File

@ -9,7 +9,7 @@ use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use typst_utils::LazyHash;
use crate::diag::{bail, StrResult};
use crate::diag::{bail, LoadError, LoadResult, StrResult};
use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value};
/// A sequence of bytes.
@ -112,6 +112,21 @@ impl Bytes {
}
}
pub fn load_str(&self) -> LoadResult<&str> {
match self.inner().as_any().downcast_ref::<Str>() {
Some(string) => Ok(string.as_str()),
None => self.as_str().map_err(|err| {
let start = err.valid_up_to();
let end = start + err.error_len().unwrap_or(0);
LoadError::new(
start..end,
"failed to convert to string",
"file is not valid utf-8",
)
}),
}
}
/// Resolve an index or throw an out of bounds error.
fn locate(&self, index: i64) -> StrResult<usize> {
self.locate_opt(index).ok_or_else(|| out_of_bounds(index, self.len()))

View File

@ -151,8 +151,8 @@ pub fn plugin(
/// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
source: Spanned<DataSource>,
) -> SourceResult<Module> {
let data = source.load(engine.world)?;
Plugin::module(data.bytes).at(source.span)
let loaded = source.load(engine.world)?;
Plugin::module(loaded.data).at(source.span)
}
#[scope]

View File

@ -23,8 +23,8 @@ pub fn cbor(
/// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
ciborium::from_reader(data.bytes.as_slice())
let loaded = source.load(engine.world)?;
ciborium::from_reader(loaded.data.as_slice())
.map_err(|err| eco_format!("failed to parse CBOR ({err})"))
.at(source.span)
}

View File

@ -1,11 +1,10 @@
use az::SaturatingAs;
use ecow::EcoVec;
use typst_syntax::Spanned;
use crate::diag::{bail, LineCol, ReportPos, SourceDiagnostic, SourceResult};
use crate::diag::{bail, LineCol, LoadError, LoadedWithin, ReportPos, SourceResult};
use crate::engine::Engine;
use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value};
use crate::loading::{DataSource, Load, Loaded, Readable};
use crate::loading::{DataSource, Load, Readable};
/// Reads structured data from a CSV file.
///
@ -45,7 +44,7 @@ pub fn csv(
#[default(RowType::Array)]
row_type: RowType,
) -> SourceResult<Array> {
let data = source.load(engine.world)?;
let loaded = source.load(engine.world)?;
let mut builder = ::csv::ReaderBuilder::new();
let has_headers = row_type == RowType::Dict;
@ -54,7 +53,7 @@ pub fn csv(
// Counting lines from 1 by default.
let mut line_offset: usize = 1;
let mut reader = builder.from_reader(data.bytes.as_slice());
let mut reader = builder.from_reader(loaded.data.as_slice());
let mut headers: Option<::csv::StringRecord> = None;
if has_headers {
@ -64,7 +63,8 @@ pub fn csv(
reader
.headers()
.cloned()
.map_err(|err| format_csv_error(&data, err, 1))?,
.map_err(|err| format_csv_error(err, 1))
.within(&loaded)?,
);
}
@ -74,7 +74,7 @@ pub fn csv(
// incorrect with `has_headers` set to `false`. See issue:
// https://github.com/BurntSushi/rust-csv/issues/184
let line = line + line_offset;
let row = result.map_err(|err| format_csv_error(&data, err, line))?;
let row = result.map_err(|err| format_csv_error(err, line)).within(&loaded)?;
let item = if let Some(headers) = &headers {
let mut dict = Dict::new();
for (field, value) in headers.iter().zip(&row) {
@ -164,11 +164,7 @@ cast! {
}
/// Format the user-facing CSV error message.
fn format_csv_error(
data: &Loaded,
err: ::csv::Error,
line: usize,
) -> EcoVec<SourceDiagnostic> {
fn format_csv_error(err: ::csv::Error, line: usize) -> LoadError {
let msg = "failed to parse CSV";
let pos = (err.kind().position())
.map(|pos| {
@ -178,13 +174,13 @@ fn format_csv_error(
.unwrap_or(LineCol::one_based(line, 1).into());
match err.kind() {
::csv::ErrorKind::Utf8 { .. } => {
data.err_in_text(pos, msg, "file is not valid utf-8")
LoadError::new(pos, msg, "file is not valid utf-8")
}
::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => {
let err =
format!("found {len} instead of {expected_len} fields in line {line}");
data.err_in_text(pos, msg, err)
LoadError::new(pos, msg, err)
}
_ => data.err_in_text(pos, "failed to parse CSV", err),
_ => LoadError::new(pos, "failed to parse CSV", err),
}
}

View File

@ -1,7 +1,7 @@
use ecow::eco_format;
use typst_syntax::Spanned;
use crate::diag::{At, LineCol, SourceResult};
use crate::diag::{At, LineCol, LoadError, LoadedWithin, SourceResult};
use crate::engine::Engine;
use crate::foundations::{func, scope, Str, Value};
use crate::loading::{DataSource, Load, Readable};
@ -54,11 +54,13 @@ pub fn json(
/// A [path]($syntax/#paths) to a JSON file or raw JSON bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
serde_json::from_slice(data.bytes.as_slice()).map_err(|err| {
let pos = LineCol::one_based(err.line(), err.column());
data.err_in_text(pos, "failed to parse JSON", err)
})
let loaded = source.load(engine.world)?;
serde_json::from_slice(loaded.data.as_slice())
.map_err(|err| {
let pos = LineCol::one_based(err.line(), err.column());
LoadError::new(pos, "failed to parse JSON", err)
})
.within(&loaded)
}
#[scope]

View File

@ -27,7 +27,7 @@ pub use self::toml_::*;
pub use self::xml_::*;
pub use self::yaml_::*;
use crate::diag::{At, LoadError, LoadResult, LoadedAt, SourceResult};
use crate::diag::{At, LoadedWithin, SourceResult};
use crate::foundations::OneOrMultiple;
use crate::foundations::{cast, Bytes, Scope, Str};
use crate::World;
@ -124,16 +124,16 @@ impl Load for Spanned<&OneOrMultiple<DataSource>> {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Loaded {
pub source: Spanned<LoadSource>,
pub bytes: Bytes,
pub data: Bytes,
}
impl Loaded {
pub fn new(source: Spanned<LoadSource>, bytes: Bytes) -> Self {
Self { source, bytes }
Self { source, data: bytes }
}
pub fn load_str(&self) -> SourceResult<&str> {
self.bytes.load_str().in_invalid_text(self)
self.data.load_str().within(self)
}
}
@ -144,24 +144,6 @@ pub enum LoadSource {
Bytes,
}
pub trait LoadStr {
fn load_str(&self) -> LoadResult<&str>;
}
impl<T: AsRef<[u8]>> LoadStr for T {
fn load_str(&self) -> LoadResult<&str> {
std::str::from_utf8(self.as_ref()).map_err(|err| {
let start = err.valid_up_to();
let end = start + err.error_len().unwrap_or(0);
LoadError::new(
start..end,
"failed to convert to string",
"file is not valid utf-8",
)
})
}
}
/// A value that can be read from a file.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Readable {

View File

@ -35,10 +35,10 @@ pub fn read(
#[default(Some(Encoding::Utf8))]
encoding: Option<Encoding>,
) -> SourceResult<Readable> {
let data = path.map(DataSource::Path).load(engine.world)?;
let loaded = path.map(DataSource::Path).load(engine.world)?;
Ok(match encoding {
None => Readable::Bytes(data.bytes),
Some(Encoding::Utf8) => Readable::Str(data.load_str()?.into()),
None => Readable::Bytes(loaded.data),
Some(Encoding::Utf8) => Readable::Str(loaded.load_str()?.into()),
})
}

View File

@ -1,7 +1,7 @@
use ecow::eco_format;
use typst_syntax::Spanned;
use crate::diag::{At, LoadError, LoadedAt, ReportPos, SourceResult};
use crate::diag::{At, LoadError, LoadedWithin, ReportPos, SourceResult};
use crate::engine::Engine;
use crate::foundations::{func, scope, Str, Value};
use crate::loading::{DataSource, Load, Readable};
@ -32,9 +32,9 @@ pub fn toml(
/// A [path]($syntax/#paths) to a TOML file or raw TOML bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
let raw = data.load_str()?;
::toml::from_str(raw).map_err(format_toml_error).in_text(&data)
let loaded = source.load(engine.world)?;
let raw = loaded.load_str()?;
::toml::from_str(raw).map_err(format_toml_error).within(&loaded)
}
#[scope]

View File

@ -1,7 +1,7 @@
use roxmltree::ParsingOptions;
use typst_syntax::Spanned;
use crate::diag::{format_xml_like_error, LoadError, LoadedAt, SourceResult};
use crate::diag::{format_xml_like_error, LoadError, LoadedWithin, SourceResult};
use crate::engine::Engine;
use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value};
use crate::loading::{DataSource, Load, Readable};
@ -60,14 +60,14 @@ pub fn xml(
/// A [path]($syntax/#paths) to an XML file or raw XML bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
let text = data.load_str()?;
let loaded = source.load(engine.world)?;
let text = loaded.load_str()?;
let document = roxmltree::Document::parse_with_options(
text,
ParsingOptions { allow_dtd: true, ..Default::default() },
)
.map_err(format_xml_error)
.in_text(&data)?;
.within(&loaded)?;
Ok(convert_xml(document.root()))
}

View File

@ -1,10 +1,10 @@
use ecow::{eco_format, EcoVec};
use ecow::eco_format;
use typst_syntax::Spanned;
use crate::diag::{At, LineCol, ReportPos, SourceDiagnostic, SourceResult};
use crate::diag::{At, LineCol, LoadError, LoadedWithin, ReportPos, SourceResult};
use crate::engine::Engine;
use crate::foundations::{func, scope, Str, Value};
use crate::loading::{DataSource, Load, Loaded, Readable};
use crate::loading::{DataSource, Load, Readable};
/// Reads structured data from a YAML file.
///
@ -44,9 +44,10 @@ pub fn yaml(
/// A [path]($syntax/#paths) to a YAML file or raw YAML bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
serde_yaml::from_slice(data.bytes.as_slice())
.map_err(|err| format_yaml_error(&data, err))
let loaded = source.load(engine.world)?;
serde_yaml::from_slice(loaded.data.as_slice())
.map_err(format_yaml_error)
.within(&loaded)
}
#[scope]
@ -76,10 +77,7 @@ impl yaml {
}
}
pub fn format_yaml_error(
data: &Loaded,
error: serde_yaml::Error,
) -> EcoVec<SourceDiagnostic> {
pub fn format_yaml_error(error: serde_yaml::Error) -> LoadError {
let pos = error
.location()
.map(|loc| {
@ -88,5 +86,5 @@ pub fn format_yaml_error(
ReportPos::full(range, line_col)
})
.unwrap_or_default();
data.err_in_text(pos, "failed to parse YAML", error)
LoadError::new(pos, "failed to parse YAML", error)
}

View File

@ -20,8 +20,8 @@ use typst_syntax::{Span, Spanned};
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
use crate::diag::{
bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedAt, ReportPos,
SourceDiagnostic, SourceResult, StrResult,
bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos,
SourceResult, StrResult,
};
use crate::engine::{Engine, Sink};
use crate::foundations::{
@ -34,7 +34,7 @@ use crate::layout::{
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
Sides, Sizing, TrackSizings,
};
use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, LoadStr, Loaded};
use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, Loaded};
use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
Url,
@ -297,8 +297,8 @@ impl Bibliography {
world: Tracked<dyn World + '_>,
sources: Spanned<OneOrMultiple<DataSource>>,
) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> {
let data = sources.load(world)?;
let bibliography = Self::decode(&data)?;
let loaded = sources.load(world)?;
let bibliography = Self::decode(&loaded)?;
Ok(Derived::new(sources.v, bibliography))
}
@ -355,10 +355,10 @@ impl Debug for Bibliography {
}
/// Decode on library from one data source.
fn decode_library(data: &Loaded) -> SourceResult<Library> {
let str = data.load_str()?;
fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
let data = loaded.load_str()?;
if let LoadSource::Path(file_id) = data.source.v {
if let LoadSource::Path(file_id) = loaded.source.v {
// If we got a path, use the extension to determine whether it is
// YAML or BibLaTeX.
let ext = file_id
@ -369,25 +369,27 @@ fn decode_library(data: &Loaded) -> SourceResult<Library> {
.unwrap_or_default();
match ext.to_lowercase().as_str() {
"yml" | "yaml" => hayagriva::io::from_yaml_str(str)
.map_err(|err| format_yaml_error(data, err)),
"bib" => hayagriva::io::from_biblatex_str(str)
.map_err(|errors| format_biblatex_error(data, errors)),
"yml" | "yaml" => hayagriva::io::from_yaml_str(data)
.map_err(format_yaml_error)
.within(loaded),
"bib" => hayagriva::io::from_biblatex_str(data)
.map_err(format_biblatex_error)
.within(loaded),
_ => bail!(
data.source.span,
loaded.source.span,
"unknown bibliography format (must be .yml/.yaml or .bib)"
),
}
} else {
// If we just got bytes, we need to guess. If it can be decoded as
// hayagriva YAML, we'll use that.
let haya_err = match hayagriva::io::from_yaml_str(str) {
let haya_err = match hayagriva::io::from_yaml_str(data) {
Ok(library) => return Ok(library),
Err(err) => err,
};
// If it can be decoded as BibLaTeX, we use that isntead.
let bib_errs = match hayagriva::io::from_biblatex_str(str) {
let bib_errs = match hayagriva::io::from_biblatex_str(data) {
// If the file is almost valid yaml, but contains no `@` character
// it will be successfully parsed as an empty BibLaTeX library,
// since BibLaTeX does support arbitrary text outside of entries.
@ -401,7 +403,7 @@ fn decode_library(data: &Loaded) -> SourceResult<Library> {
// and emit the more appropriate error.
let mut yaml = 0;
let mut biblatex = 0;
for c in str.chars() {
for c in data.chars() {
match c {
':' => yaml += 1,
'{' => biblatex += 1,
@ -411,22 +413,19 @@ fn decode_library(data: &Loaded) -> SourceResult<Library> {
match bib_errs {
Some(bib_errs) if biblatex >= yaml => {
Err(format_biblatex_error(data, bib_errs))
Err(format_biblatex_error(bib_errs)).within(&loaded)
}
_ => Err(format_yaml_error(data, haya_err)),
_ => Err(format_yaml_error(haya_err)).within(&loaded),
}
}
}
/// Format a BibLaTeX loading error.
fn format_biblatex_error(
data: &Loaded,
errors: Vec<BibLaTeXError>,
) -> EcoVec<SourceDiagnostic> {
fn format_biblatex_error(errors: Vec<BibLaTeXError>) -> LoadError {
// TODO: return multiple errors?
let Some(error) = errors.into_iter().next() else {
// TODO: can this even happen, should we just unwrap?
return data.err_in_text(ReportPos::None, "failed to parse BibLaTeX", "???");
return LoadError::new(ReportPos::None, "failed to parse BibLaTeX", "???");
};
let (range, msg) = match error {
@ -434,7 +433,7 @@ fn format_biblatex_error(
BibLaTeXError::Type(error) => (error.span, error.kind.to_string()),
};
data.err_in_text(range, "failed to parse BibLaTeX", msg)
LoadError::new(range, "failed to parse BibLaTeX", msg)
}
/// A loaded CSL style.
@ -450,8 +449,8 @@ impl CslStyle {
let style = match &source {
CslSource::Named(style) => Self::from_archived(*style),
CslSource::Normal(source) => {
let data = Spanned::new(source, span).load(world)?;
Self::from_data(&data.bytes).in_text(&data)?
let loaded = Spanned::new(source, span).load(world)?;
Self::from_data(&loaded.data).within(&loaded)?
}
};
Ok(Derived::new(source, style))

View File

@ -11,7 +11,7 @@ use typst_utils::ManuallyHash;
use unicode_segmentation::UnicodeSegmentation;
use super::Lang;
use crate::diag::{LineCol, LoadError, LoadResult, LoadedAt, ReportPos, SourceResult};
use crate::diag::{LineCol, LoadError, LoadResult, LoadedWithin, ReportPos, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed,
@ -19,7 +19,7 @@ use crate::foundations::{
};
use crate::html::{tag, HtmlElem};
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::loading::{DataSource, Load, LoadStr};
use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem};
use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize};
use crate::visualize::Color;
@ -539,10 +539,10 @@ impl RawSyntax {
world: Tracked<dyn World + '_>,
sources: Spanned<OneOrMultiple<DataSource>>,
) -> SourceResult<Derived<OneOrMultiple<DataSource>, Vec<RawSyntax>>> {
let data = sources.load(world)?;
let list = data
let loaded = sources.load(world)?;
let list = loaded
.iter()
.map(|data| Self::decode(&data.bytes).in_text(data))
.map(|data| Self::decode(&data.data).within(data))
.collect::<SourceResult<_>>()?;
Ok(Derived::new(sources.v, list))
}
@ -599,8 +599,8 @@ impl RawTheme {
world: Tracked<dyn World + '_>,
source: Spanned<DataSource>,
) -> SourceResult<Derived<DataSource, Self>> {
let data = source.load(world)?;
let theme = Self::decode(&data.bytes).in_text(&data)?;
let loaded = source.load(world)?;
let theme = Self::decode(&loaded.data).within(&loaded)?;
Ok(Derived::new(source.v, theme))
}

View File

@ -65,8 +65,8 @@ pub struct ImageElem {
#[required]
#[parse(
let source = args.expect::<Spanned<DataSource>>("source")?;
let data = source.load(engine.world)?;
Derived::new(source.v, data)
let loaded = source.load(engine.world)?;
Derived::new(source.v, loaded)
)]
pub source: Derived<DataSource, Loaded>,
@ -154,8 +154,8 @@ pub struct ImageElem {
/// to `{auto}`, Typst will try to extract an ICC profile from the image.
#[parse(match args.named::<Spanned<Smart<DataSource>>>("icc")? {
Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({
let data = Spanned::new(&source, span).load(engine.world)?;
Derived::new(source, data.bytes)
let loaded = Spanned::new(&source, span).load(engine.world)?;
Derived::new(source, loaded.data)
})),
Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto),
None => None,
@ -194,8 +194,8 @@ impl ImageElem {
scaling: Option<Smart<ImageScaling>>,
) -> StrResult<Content> {
let bytes = data.v.into_bytes();
let data = Loaded::new(Spanned::new(LoadSource::Bytes, data.span), bytes.clone());
let source = Derived::new(DataSource::Bytes(bytes), data);
let loaded = Loaded::new(Spanned::new(LoadSource::Bytes, data.span), bytes.clone());
let source = Derived::new(DataSource::Bytes(bytes), loaded);
let mut elem = ImageElem::new(source);
if let Some(format) = format {
elem.push_format(format);