Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions crates/typst-layout/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,22 @@ pub fn layout_image(
}
}

// Resolve the image file ID, but only if the path is present.
let mut id = None;
if !elem.path.is_empty() {
id = Some(span.resolve_path(&elem.path).at(span)?);
}

// Construct the image itself.
let image = Image::with_fonts(
data.clone().into(),
format,
elem.alt(styles),
engine.world,
&families(styles).collect::<Vec<_>>(),
)
.at(span)?;
span,
id,
)?;

// Determine the image's pixel aspect ratio.
let pxw = image.width();
Expand Down
37 changes: 28 additions & 9 deletions crates/typst-library/src/diag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::string::FromUtf8Error;
use comemo::Tracked;
use ecow::{eco_vec, EcoVec};
use typst_syntax::package::{PackageSpec, PackageVersion};
use typst_syntax::{Span, Spanned, SyntaxError};
use typst_syntax::{FileId, Span, Spanned, SyntaxError};

use crate::{World, WorldExt};

Expand Down Expand Up @@ -551,30 +551,49 @@ impl From<PackageError> for EcoString {
}

/// Format a user-facing error message for an XML-like file format.
pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString {
match error {
pub fn format_xml_like_error(
format: &str,
error: roxmltree::Error,
call_span: Span,
file: Option<FileId>,
text: &str,
) -> EcoVec<SourceDiagnostic> {
let span = file
.and_then(|id| {
let pos = error.pos();
let pos =
(pos.row.saturating_sub(1) as usize, pos.col.saturating_sub(1) as usize);

Span::from_row_column(id, pos, pos, text)
})
.unwrap_or(call_span);

eco_vec![match error {
roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => {
eco_format!(
error!(
span,
"failed to parse {format} (found closing tag '{actual}' \
instead of '{expected}' in line {})",
pos.row
)
}
roxmltree::Error::UnknownEntityReference(entity, pos) => {
eco_format!(
error!(
span,
"failed to parse {format} (unknown entity '{entity}' in line {})",
pos.row
)
}
roxmltree::Error::DuplicatedAttribute(attr, pos) => {
eco_format!(
error!(
span,
"failed to parse {format} (duplicate attribute '{attr}' in line {})",
pos.row
)
}
roxmltree::Error::NoRootNode => {
eco_format!("failed to parse {format} (missing root node)")
error!(span, "failed to parse {format} (missing root node)")
}
err => eco_format!("failed to parse {format} ({err})"),
}
err => error!(span, "failed to parse {format} ({err})"),
}]
}
124 changes: 71 additions & 53 deletions crates/typst-library/src/loading/csv.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use ecow::{eco_format, EcoString};
use typst_syntax::Spanned;
use ecow::{eco_vec, EcoString, EcoVec};
use typst_syntax::{FileId, Span, Spanned};

use crate::diag::{bail, At, SourceResult};
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use crate::engine::Engine;
use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value};
use crate::loading::Readable;
Expand Down Expand Up @@ -51,7 +51,7 @@ pub fn csv(
let Spanned { v: path, span } = path;
let id = span.resolve_path(&path).at(span)?;
let data = engine.world.file(id).at(span)?;
self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter, row_type)
decode_inner(Spanned::new(Readable::Bytes(data), span), delimiter, row_type, Some(id))
}

#[scope]
Expand All @@ -77,52 +77,60 @@ impl csv {
#[default(RowType::Array)]
row_type: RowType,
) -> SourceResult<Array> {
let Spanned { v: data, span } = data;
let has_headers = row_type == RowType::Dict;

let mut builder = ::csv::ReaderBuilder::new();
builder.has_headers(has_headers);
builder.delimiter(delimiter.0 as u8);

// Counting lines from 1 by default.
let mut line_offset: usize = 1;
let mut reader = builder.from_reader(data.as_slice());
let mut headers: Option<::csv::StringRecord> = None;

if has_headers {
// Counting lines from 2 because we have a header.
line_offset += 1;
headers = Some(
reader
.headers()
.map_err(|err| format_csv_error(err, 1))
.at(span)?
.clone(),
);
}
decode_inner(data, delimiter, row_type, None)
}
}

let mut array = Array::new();
for (line, result) in reader.records().enumerate() {
// Original solution was to use line from error, but that is
// 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(err, line)).at(span)?;
let item = if let Some(headers) = &headers {
let mut dict = Dict::new();
for (field, value) in headers.iter().zip(&row) {
dict.insert(field.into(), value.into_value());
}
dict.into_value()
} else {
let sub = row.into_iter().map(|field| field.into_value()).collect();
Value::Array(sub)
};
array.push(item);
}
fn decode_inner(
data: Spanned<Readable>,
delimiter: Delimiter,
row_type: RowType,
source: Option<FileId>,
) -> SourceResult<Array> {
let Spanned { v: data, span } = data;
let has_headers = row_type == RowType::Dict;

let mut builder = ::csv::ReaderBuilder::new();
builder.has_headers(has_headers);
builder.delimiter(delimiter.0 as u8);

// Counting lines from 1 by default.
let mut line_offset: usize = 1;
let mut reader = builder.from_reader(data.as_slice());
let mut headers: Option<::csv::StringRecord> = None;

if has_headers {
// Counting lines from 2 because we have a header.
line_offset += 1;
headers = Some(
reader
.headers()
.map_err(|err| format_csv_error(err, 1, span, source))?
.clone(),
);
}

Ok(array)
let mut array = Array::new();
for (line, result) in reader.records().enumerate() {
// Original solution was to use line from error, but that is
// 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(err, line, span, source))?;
let item = if let Some(headers) = &headers {
let mut dict = Dict::new();
for (field, value) in headers.iter().zip(&row) {
dict.insert(field.into(), value.into_value());
}
dict.into_value()
} else {
let sub = row.into_iter().map(|field| field.into_value()).collect();
Value::Array(sub)
};
array.push(item);
}

Ok(array)
}

/// The delimiter to use when parsing CSV files.
Expand Down Expand Up @@ -177,15 +185,25 @@ cast! {
}

/// Format the user-facing CSV error message.
fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString {
match err.kind() {
::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(),
fn format_csv_error(
err: ::csv::Error,
line: usize,
call_span: Span,
file_id: Option<FileId>,
) -> EcoVec<SourceDiagnostic> {
let span = file_id
.and_then(|id| err.position().map(|pos| (id, pos.byte() as usize)))
.map_or(call_span, |(id, pos)| Span::from_range(id, pos..pos));

eco_vec![match err.kind() {
::csv::ErrorKind::Utf8 { .. } => error!(span, "file is not valid utf-8"),
::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => {
eco_format!(
error!(
span,
"failed to parse CSV (found {len} instead of \
{expected_len} fields in line {line})"
)
}
_ => eco_format!("failed to parse CSV ({err})"),
}
_ => error!(span, "failed to parse CSV ({err})"),
}]
}
21 changes: 17 additions & 4 deletions crates/typst-library/src/loading/json.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use ecow::{eco_format, EcoString};
use typst_syntax::Spanned;
use ecow::{eco_format, eco_vec, EcoString};
use typst_syntax::{Span, Spanned};

use crate::diag::{At, SourceResult};
use crate::diag::{self, At, SourceResult};
use crate::engine::Engine;
use crate::foundations::{func, scope, Str, Value};
use crate::loading::Readable;
Expand Down Expand Up @@ -61,7 +61,20 @@ pub fn json(
let Spanned { v: path, span } = path;
let id = span.resolve_path(&path).at(span)?;
let data = engine.world.file(id).at(span)?;
json::decode(Spanned::new(Readable::Bytes(data), span))

serde_json::from_slice(data.as_slice()).map_err(|err| {
let loc = (err.line() - 1, err.column().saturating_sub(1));
eco_vec![diag::error!(
Span::from_row_column(
id,
loc,
loc,
&String::from_utf8_lossy(data.as_slice())
)
.unwrap_or(span),
"failed to parse JSON ({err})"
)]
})
}

#[scope]
Expand Down
31 changes: 22 additions & 9 deletions crates/typst-library/src/loading/toml.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use ecow::{eco_format, EcoString};
use typst_syntax::{is_newline, Spanned};
use ecow::{eco_format, eco_vec, EcoString};
use typst_syntax::{is_newline, FileId, Span, Spanned};

use crate::diag::{At, SourceResult};
use crate::diag::{error, At, SourceDiagnostic, SourceResult};
use crate::engine::Engine;
use crate::foundations::{func, scope, Str, Value};
use crate::loading::Readable;
Expand Down Expand Up @@ -39,7 +39,11 @@ pub fn toml(
let Spanned { v: path, span } = path;
let id = span.resolve_path(&path).at(span)?;
let data = engine.world.file(id).at(span)?;
toml::decode(Spanned::new(Readable::Bytes(data), span))
let raw = std::str::from_utf8(data.as_slice())
.map_err(|_| "file is not valid utf-8")
.at(span)?;
::toml::from_str(raw)
.map_err(|err| eco_vec![format_toml_error(err, raw, span, Some(id))])
}

#[scope]
Expand All @@ -55,8 +59,7 @@ impl toml {
.map_err(|_| "file is not valid utf-8")
.at(span)?;
::toml::from_str(raw)
.map_err(|err| format_toml_error(err, raw))
.at(span)
.map_err(|err| eco_vec![format_toml_error(err, raw, span, None)])
}

/// Encodes structured data into a TOML string.
Expand All @@ -78,15 +81,25 @@ impl toml {
}

/// Format the user-facing TOML error message.
fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString {
fn format_toml_error(
error: ::toml::de::Error,
raw: &str,
call_span: Span,
file_id: Option<FileId>,
) -> SourceDiagnostic {
let span = file_id
.and_then(|id| error.span().map(|range| (id, range)))
.map_or(call_span, |(id, range)| Span::from_range(id, range));

if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) {
let line = head.lines().count();
let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count();
eco_format!(
error!(
span,
"failed to parse TOML ({} at line {line} column {column})",
error.message(),
)
} else {
eco_format!("failed to parse TOML ({})", error.message())
error!(span, "failed to parse TOML ({})", error.message())
}
}
Loading