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() source.lines()
} else if let Some(bytes) = slot.file.get() { } else if let Some(bytes) = slot.file.get() {
let bytes = bytes.as_ref().expect("file is not valid"); 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 { } else {
panic!("file id does not point to any source file"); panic!("file id does not point to any source file");
} }

View File

@ -1,6 +1,6 @@
use std::ffi::OsStr; 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::engine::Engine;
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
@ -27,17 +27,17 @@ pub fn layout_image(
// Take the format that was explicitly defined, or parse the extension, // Take the format that was explicitly defined, or parse the extension,
// or try to detect the format. // 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) { let format = match elem.format(styles) {
Smart::Custom(v) => v, 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 // 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. // because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) { if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object = 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 { if has_foreign_object {
engine.sink.warn(warning!( engine.sink.warn(warning!(
@ -53,7 +53,7 @@ pub fn layout_image(
let kind = match format { let kind = match format {
ImageFormat::Raster(format) => ImageKind::Raster( ImageFormat::Raster(format) => ImageKind::Raster(
RasterImage::new( RasterImage::new(
data.bytes.clone(), loaded.data.clone(),
format, format,
elem.icc(styles).as_ref().map(|icc| icc.derived.clone()), elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
) )
@ -61,11 +61,11 @@ pub fn layout_image(
), ),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts( SvgImage::with_fonts(
data.bytes.clone(), loaded.data.clone(),
engine.world, engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(), &families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
) )
.in_text(data)?, .within(loaded)?,
), ),
}; };

View File

@ -1,6 +1,6 @@
//! Diagnostics. //! Diagnostics.
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter, Write as _};
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::Utf8Error; use std::str::Utf8Error;
@ -581,9 +581,9 @@ pub type LoadResult<T> = Result<T, LoadError>;
/// [`FileId`]: typst_syntax::FileId /// [`FileId`]: typst_syntax::FileId
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LoadError { pub struct LoadError {
pub pos: ReportPos, pos: ReportPos,
pub message: EcoString, /// Must contain a message formatted like this: `"failed to do thing (cause)"`.
pub error: EcoString, message: EcoString,
} }
impl LoadError { impl LoadError {
@ -594,109 +594,98 @@ impl LoadError {
) -> Self { ) -> Self {
Self { Self {
pos: pos.into(), pos: pos.into(),
message: eco_format!("{message}"), message: eco_format!("{message} ({error})"),
error: eco_format!("{error}"),
} }
} }
} }
/// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`] context. /// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`] context.
pub trait LoadedAt<T> { pub trait LoadedWithin<T> {
/// Add the span information. /// Report an error, possibly in an external file.
fn in_text(self, data: &Loaded) -> SourceResult<T>; fn within(self, loaded: &Loaded) -> SourceResult<T>;
/// Add the span information.
fn in_invalid_text(self, data: &Loaded) -> SourceResult<T>;
} }
impl<T> LoadedAt<T> for Result<T, LoadError> { impl<T> LoadedWithin<T> for Result<T, LoadError> {
/// Report an error, possibly in an external file. fn within(self, loaded: &Loaded) -> SourceResult<T> {
fn in_text(self, data: &Loaded) -> SourceResult<T> { self.map_err(|err| load_err_in_text(loaded, err.pos, err.message))
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 Loaded { /// Report an error, possibly in an external file. This will delegate to
/// Report an error, possibly in an external file. /// [`load_err_in_invalid_text`] if the data isn't valid utf-8.
pub fn err_in_text( fn load_err_in_text(
&self, loaded: &Loaded,
pos: impl Into<ReportPos>, pos: impl Into<ReportPos>,
msg: impl std::fmt::Display, mut message: EcoString,
error: impl std::fmt::Display, ) -> EcoVec<SourceDiagnostic> {
) -> EcoVec<SourceDiagnostic> { let pos = pos.into();
let pos = pos.into(); // This also does utf-8 validation. Only report an error in an external
// 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
// file if it is human readable (valid utf-8), otherwise fall back to // `load_err_in_invalid_text`.
// `err_in_invalid_text`. let lines = Lines::from_bytes(&loaded.data);
let lines = Lines::from_bytes(&self.bytes); match (loaded.source.v, lines) {
match (self.source.v, lines) { (LoadSource::Path(file_id), Ok(lines)) => {
(LoadSource::Path(file_id), Ok(lines)) => { if let Some(range) = pos.range(&lines) {
if let Some(range) = pos.range(&lines) { let span = Span::from_range(file_id, range);
let span = Span::from_range(file_id, range); return eco_vec![SourceDiagnostic::error(span, message)];
return eco_vec!(error!(span, "{msg} ({error})")); }
}
// Either `ReportPos::None` was provided, or resolving the range // Either `ReportPos::None` was provided, or resolving the range
// from the line/column failed. If present report the possibly // from the line/column failed. If present report the possibly
// wrong line/column in the error message anyway. // wrong line/column in the error message anyway.
let span = Span::from_range(file_id, 0..self.bytes.len()); let span = Span::from_range(file_id, 0..loaded.data.len());
let error = if let Some(pair) = pos.line_col(&lines) { if let Some(pair) = pos.line_col(&lines) {
let (line, col) = pair.numbers(); message.pop();
error!(span, "{msg} ({error} at {line}:{col})") let (line, col) = pair.numbers();
} else { write!(&mut message, " at {line}:{col})").ok();
error!(span, "{msg} ({error})")
};
eco_vec![error]
} }
(LoadSource::Bytes, Ok(lines)) => { eco_vec![SourceDiagnostic::error(span, message)]
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),
} }
(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. /// Report an error (possibly from an external file) that isn't valid utf-8.
pub fn err_in_invalid_text( fn load_err_in_invalid_text(
&self, loaded: &Loaded,
pos: impl Into<ReportPos>, pos: impl Into<ReportPos>,
msg: impl std::fmt::Display, mut message: EcoString,
error: impl std::fmt::Display, ) -> EcoVec<SourceDiagnostic> {
) -> EcoVec<SourceDiagnostic> { let line_col = pos.into().try_line_col(&loaded.data).map(|p| p.numbers());
let line_col = pos.into().try_line_col(&self.bytes).map(|p| p.numbers()); match (loaded.source.v, line_col) {
let error = match (self.source.v, line_col) { (LoadSource::Path(file), _) => {
(LoadSource::Path(file), _) => { message.pop();
let path = if let Some(package) = file.package() { if let Some(package) = file.package() {
format!("{package}{}", file.vpath().as_rooted_path().display()) write!(
} else { &mut message,
format!("{}", file.vpath().as_rootless_path().display()) " in {package}{}",
}; file.vpath().as_rooted_path().display()
)
if let Some((line, col)) = line_col { .ok();
error!(self.source.span, "{msg} ({error} in {path}:{line}:{col})") } else {
} else { write!(&mut message, " in {}", file.vpath().as_rootless_path().display())
error!(self.source.span, "{msg} ({error} in {path})") .ok();
} };
if let Some((line, col)) = line_col {
write!(&mut message, ":{line}:{col}").ok();
} }
(LoadSource::Bytes, Some((line, col))) => { message.push(')');
error!(self.source.span, "{msg} ({error} at {line}:{col})") }
} (LoadSource::Bytes, Some((line, col))) => {
(LoadSource::Bytes, None) => { message.pop();
error!(self.source.span, "{msg} ({error})") write!(&mut message, " at {line}:{col})").ok();
} }
}; (LoadSource::Bytes, None) => (),
eco_vec![error]
} }
eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
} }
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] #[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. /// Format a user-facing error message for an XML-like file format.
pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError { 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 pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
let message = eco_format!("failed to parse {format}"); let message = match error {
let error = match error {
roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => { 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, _) => { roxmltree::Error::UnknownEntityReference(entity, _) => {
eco_format!("unknown entity '{entity}'") eco_format!("failed to parse {format} (unknown entity '{entity}')")
} }
roxmltree::Error::DuplicatedAttribute(attr, _) => { roxmltree::Error::DuplicatedAttribute(attr, _) => {
eco_format!("duplicate attribute '{attr}'") eco_format!("failed to parse {format} (duplicate attribute '{attr}')")
} }
roxmltree::Error::NoRootNode => { 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 serde::{Serialize, Serializer};
use typst_utils::LazyHash; 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}; use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value};
/// A sequence of bytes. /// 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. /// Resolve an index or throw an out of bounds error.
fn locate(&self, index: i64) -> StrResult<usize> { fn locate(&self, index: i64) -> StrResult<usize> {
self.locate_opt(index).ok_or_else(|| out_of_bounds(index, self.len())) 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. /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Module> { ) -> SourceResult<Module> {
let data = source.load(engine.world)?; let loaded = source.load(engine.world)?;
Plugin::module(data.bytes).at(source.span) Plugin::module(loaded.data).at(source.span)
} }
#[scope] #[scope]

View File

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

View File

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

View File

@ -27,7 +27,7 @@ pub use self::toml_::*;
pub use self::xml_::*; pub use self::xml_::*;
pub use self::yaml_::*; 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::OneOrMultiple;
use crate::foundations::{cast, Bytes, Scope, Str}; use crate::foundations::{cast, Bytes, Scope, Str};
use crate::World; use crate::World;
@ -124,16 +124,16 @@ impl Load for Spanned<&OneOrMultiple<DataSource>> {
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Loaded { pub struct Loaded {
pub source: Spanned<LoadSource>, pub source: Spanned<LoadSource>,
pub bytes: Bytes, pub data: Bytes,
} }
impl Loaded { impl Loaded {
pub fn new(source: Spanned<LoadSource>, bytes: Bytes) -> Self { pub fn new(source: Spanned<LoadSource>, bytes: Bytes) -> Self {
Self { source, bytes } Self { source, data: bytes }
} }
pub fn load_str(&self) -> SourceResult<&str> { 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, 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. /// A value that can be read from a file.
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub enum Readable { pub enum Readable {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
use roxmltree::ParsingOptions; use roxmltree::ParsingOptions;
use typst_syntax::Spanned; 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::engine::Engine;
use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value};
use crate::loading::{DataSource, Load, Readable}; 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. /// A [path]($syntax/#paths) to an XML file or raw XML bytes.
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let loaded = source.load(engine.world)?;
let text = data.load_str()?; let text = loaded.load_str()?;
let document = roxmltree::Document::parse_with_options( let document = roxmltree::Document::parse_with_options(
text, text,
ParsingOptions { allow_dtd: true, ..Default::default() }, ParsingOptions { allow_dtd: true, ..Default::default() },
) )
.map_err(format_xml_error) .map_err(format_xml_error)
.in_text(&data)?; .within(&loaded)?;
Ok(convert_xml(document.root())) 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 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::engine::Engine;
use crate::foundations::{func, scope, Str, Value}; 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. /// 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. /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes.
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let loaded = source.load(engine.world)?;
serde_yaml::from_slice(data.bytes.as_slice()) serde_yaml::from_slice(loaded.data.as_slice())
.map_err(|err| format_yaml_error(&data, err)) .map_err(format_yaml_error)
.within(&loaded)
} }
#[scope] #[scope]
@ -76,10 +77,7 @@ impl yaml {
} }
} }
pub fn format_yaml_error( pub fn format_yaml_error(error: serde_yaml::Error) -> LoadError {
data: &Loaded,
error: serde_yaml::Error,
) -> EcoVec<SourceDiagnostic> {
let pos = error let pos = error
.location() .location()
.map(|loc| { .map(|loc| {
@ -88,5 +86,5 @@ pub fn format_yaml_error(
ReportPos::full(range, line_col) ReportPos::full(range, line_col)
}) })
.unwrap_or_default(); .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 typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
use crate::diag::{ use crate::diag::{
bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedAt, ReportPos, bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos,
SourceDiagnostic, SourceResult, StrResult, SourceResult, StrResult,
}; };
use crate::engine::{Engine, Sink}; use crate::engine::{Engine, Sink};
use crate::foundations::{ use crate::foundations::{
@ -34,7 +34,7 @@ use crate::layout::{
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
Sides, Sizing, TrackSizings, 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::{ use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
Url, Url,
@ -297,8 +297,8 @@ impl Bibliography {
world: Tracked<dyn World + '_>, world: Tracked<dyn World + '_>,
sources: Spanned<OneOrMultiple<DataSource>>, sources: Spanned<OneOrMultiple<DataSource>>,
) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> { ) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> {
let data = sources.load(world)?; let loaded = sources.load(world)?;
let bibliography = Self::decode(&data)?; let bibliography = Self::decode(&loaded)?;
Ok(Derived::new(sources.v, bibliography)) Ok(Derived::new(sources.v, bibliography))
} }
@ -355,10 +355,10 @@ impl Debug for Bibliography {
} }
/// Decode on library from one data source. /// Decode on library from one data source.
fn decode_library(data: &Loaded) -> SourceResult<Library> { fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
let str = data.load_str()?; 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 // If we got a path, use the extension to determine whether it is
// YAML or BibLaTeX. // YAML or BibLaTeX.
let ext = file_id let ext = file_id
@ -369,25 +369,27 @@ fn decode_library(data: &Loaded) -> SourceResult<Library> {
.unwrap_or_default(); .unwrap_or_default();
match ext.to_lowercase().as_str() { match ext.to_lowercase().as_str() {
"yml" | "yaml" => hayagriva::io::from_yaml_str(str) "yml" | "yaml" => hayagriva::io::from_yaml_str(data)
.map_err(|err| format_yaml_error(data, err)), .map_err(format_yaml_error)
"bib" => hayagriva::io::from_biblatex_str(str) .within(loaded),
.map_err(|errors| format_biblatex_error(data, errors)), "bib" => hayagriva::io::from_biblatex_str(data)
.map_err(format_biblatex_error)
.within(loaded),
_ => bail!( _ => bail!(
data.source.span, loaded.source.span,
"unknown bibliography format (must be .yml/.yaml or .bib)" "unknown bibliography format (must be .yml/.yaml or .bib)"
), ),
} }
} else { } else {
// If we just got bytes, we need to guess. If it can be decoded as // If we just got bytes, we need to guess. If it can be decoded as
// hayagriva YAML, we'll use that. // 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), Ok(library) => return Ok(library),
Err(err) => err, Err(err) => err,
}; };
// If it can be decoded as BibLaTeX, we use that isntead. // 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 // If the file is almost valid yaml, but contains no `@` character
// it will be successfully parsed as an empty BibLaTeX library, // it will be successfully parsed as an empty BibLaTeX library,
// since BibLaTeX does support arbitrary text outside of entries. // 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. // and emit the more appropriate error.
let mut yaml = 0; let mut yaml = 0;
let mut biblatex = 0; let mut biblatex = 0;
for c in str.chars() { for c in data.chars() {
match c { match c {
':' => yaml += 1, ':' => yaml += 1,
'{' => biblatex += 1, '{' => biblatex += 1,
@ -411,22 +413,19 @@ fn decode_library(data: &Loaded) -> SourceResult<Library> {
match bib_errs { match bib_errs {
Some(bib_errs) if biblatex >= yaml => { 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. /// Format a BibLaTeX loading error.
fn format_biblatex_error( fn format_biblatex_error(errors: Vec<BibLaTeXError>) -> LoadError {
data: &Loaded,
errors: Vec<BibLaTeXError>,
) -> EcoVec<SourceDiagnostic> {
// TODO: return multiple errors? // TODO: return multiple errors?
let Some(error) = errors.into_iter().next() else { let Some(error) = errors.into_iter().next() else {
// TODO: can this even happen, should we just unwrap? // 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 { let (range, msg) = match error {
@ -434,7 +433,7 @@ fn format_biblatex_error(
BibLaTeXError::Type(error) => (error.span, error.kind.to_string()), 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. /// A loaded CSL style.
@ -450,8 +449,8 @@ impl CslStyle {
let style = match &source { let style = match &source {
CslSource::Named(style) => Self::from_archived(*style), CslSource::Named(style) => Self::from_archived(*style),
CslSource::Normal(source) => { CslSource::Normal(source) => {
let data = Spanned::new(source, span).load(world)?; let loaded = Spanned::new(source, span).load(world)?;
Self::from_data(&data.bytes).in_text(&data)? Self::from_data(&loaded.data).within(&loaded)?
} }
}; };
Ok(Derived::new(source, style)) Ok(Derived::new(source, style))

View File

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

View File

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