refactor: implement TryFrom<&Bytes> for Lines

- remove Bytes::load_str and impl From<Utf8Error> for LoadError
This commit is contained in:
Tobias Schmitz 2025-06-10 13:03:40 +02:00
parent 3bde9cf52d
commit 0ed6b31b70
No known key found for this signature in database
9 changed files with 44 additions and 39 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).expect("file is not valid utf-8") Lines::try_from(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

@ -599,6 +599,18 @@ impl LoadError {
} }
} }
impl From<Utf8Error> for LoadError {
fn from(err: Utf8Error) -> Self {
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",
)
}
}
/// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`] context. /// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`] context.
pub trait LoadedWithin<T> { pub trait LoadedWithin<T> {
/// Report an error, possibly in an external file. /// Report an error, possibly in an external file.
@ -622,7 +634,7 @@ fn load_err_in_text(
// 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`. // `load_err_in_invalid_text`.
let lines = Lines::from_bytes(&loaded.data); let lines = Lines::try_from(&loaded.data);
match (loaded.source.v, lines) { match (loaded.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) {
@ -784,6 +796,7 @@ impl LineCol {
pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option<Self> { pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option<Self> {
let bytes = &bytes[..pos]; let bytes = &bytes[..pos];
let mut line = 0; let mut line = 0;
#[allow(clippy::double_ended_iterator_last)]
let line_start = memchr::memchr_iter(b'\n', bytes) let line_start = memchr::memchr_iter(b'\n', bytes)
.inspect(|_| line += 1) .inspect(|_| line += 1)
.last() .last()

View File

@ -7,9 +7,10 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use typst_syntax::Lines;
use typst_utils::LazyHash; use typst_utils::LazyHash;
use crate::diag::{bail, LoadError, LoadResult, StrResult}; use crate::diag::{bail, 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,21 +113,6 @@ 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()))
@ -301,6 +287,16 @@ impl Serialize for Bytes {
} }
} }
impl TryFrom<&Bytes> for Lines<String> {
type Error = Utf8Error;
#[comemo::memoize]
fn try_from(value: &Bytes) -> Result<Lines<String>, Utf8Error> {
let text = value.as_str()?;
Ok(Lines::new(text.to_string()))
}
}
/// Any type that can back a byte buffer. /// Any type that can back a byte buffer.
trait Bytelike: Send + Sync { trait Bytelike: Send + Sync {
fn as_bytes(&self) -> &[u8]; fn as_bytes(&self) -> &[u8];

View File

@ -133,7 +133,7 @@ impl Loaded {
} }
pub fn load_str(&self) -> SourceResult<&str> { pub fn load_str(&self) -> SourceResult<&str> {
self.data.load_str().within(self) self.data.as_str().map_err(Into::into).within(self)
} }
} }

View File

@ -413,9 +413,9 @@ fn decode_library(loaded: &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(bib_errs)).within(&loaded) Err(format_biblatex_error(bib_errs)).within(loaded)
} }
_ => Err(format_yaml_error(haya_err)).within(&loaded), _ => Err(format_yaml_error(haya_err)).within(loaded),
} }
} }
} }
@ -472,7 +472,7 @@ impl CslStyle {
/// Load a CSL style from file contents. /// Load a CSL style from file contents.
#[comemo::memoize] #[comemo::memoize]
pub fn from_data(bytes: &Bytes) -> LoadResult<CslStyle> { pub fn from_data(bytes: &Bytes) -> LoadResult<CslStyle> {
let text = bytes.load_str()?; let text = bytes.as_str()?;
citationberg::IndependentStyle::from_xml(text) citationberg::IndependentStyle::from_xml(text)
.map(|style| { .map(|style| {
Self(Arc::new(ManuallyHash::new( Self(Arc::new(ManuallyHash::new(

View File

@ -553,7 +553,7 @@ impl RawSyntax {
#[comemo::memoize] #[comemo::memoize]
#[typst_macros::time(name = "load syntaxes")] #[typst_macros::time(name = "load syntaxes")]
fn decode(bytes: &Bytes) -> LoadResult<RawSyntax> { fn decode(bytes: &Bytes) -> LoadResult<RawSyntax> {
let str = bytes.load_str()?; let str = bytes.as_str()?;
let syntax = SyntaxDefinition::load_from_str(str, false, None) let syntax = SyntaxDefinition::load_from_str(str, false, None)
.map_err(format_syntax_error)?; .map_err(format_syntax_error)?;

View File

@ -1,7 +1,6 @@
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::iter::zip; use std::iter::zip;
use std::ops::Range; use std::ops::Range;
use std::str::Utf8Error;
use std::sync::Arc; use std::sync::Arc;
use crate::is_newline; use crate::is_newline;
@ -11,9 +10,9 @@ use crate::is_newline;
pub struct Lines<S>(Arc<Repr<S>>); pub struct Lines<S>(Arc<Repr<S>>);
#[derive(Clone)] #[derive(Clone)]
struct Repr<S> { struct Repr<T> {
lines: Vec<Line>, lines: Vec<Line>,
text: S, text: T,
} }
/// Metadata about a line. /// Metadata about a line.
@ -25,12 +24,14 @@ pub struct Line {
utf16_idx: usize, utf16_idx: usize,
} }
impl<S: AsRef<str>> Lines<S> { impl<T: AsRef<str>> Lines<T> {
pub fn new(text: S) -> Self { /// Create from the text buffer and compute the line metadata.
pub fn new(text: T) -> Self {
let lines = lines(text.as_ref()); let lines = lines(text.as_ref());
Lines(Arc::new(Repr { lines, text })) Lines(Arc::new(Repr { lines, text }))
} }
/// The text as a string slice.
pub fn text(&self) -> &str { pub fn text(&self) -> &str {
self.0.text.as_ref() self.0.text.as_ref()
} }
@ -142,13 +143,6 @@ impl<S: AsRef<str>> Lines<S> {
} }
impl Lines<String> { impl Lines<String> {
/// Tries to convert the bytes
#[comemo::memoize]
pub fn from_bytes(bytes: &[u8]) -> Result<Lines<String>, Utf8Error> {
let text = std::str::from_utf8(bytes)?;
Ok(Lines::new(text.to_string()))
}
/// Fully replace the source text. /// Fully replace the source text.
/// ///
/// This performs a naive (suffix/prefix-based) diff of the old and new text /// This performs a naive (suffix/prefix-based) diff of the old and new text

View File

@ -397,6 +397,8 @@ impl<'a> Parser<'a> {
/// if the range is empty. /// if the range is empty.
#[cfg(feature = "default")] #[cfg(feature = "default")]
fn parse_range_external(&mut self, file: FileId) -> Option<Range<usize>> { fn parse_range_external(&mut self, file: FileId) -> Option<Range<usize>> {
use typst::foundations::Bytes;
use crate::world::{read, system_path}; use crate::world::{read, system_path};
let path = match system_path(file) { let path = match system_path(file) {
@ -407,8 +409,8 @@ impl<'a> Parser<'a> {
} }
}; };
let text = match read(&path) { let bytes = match read(&path) {
Ok(text) => text, Ok(data) => Bytes::new(data),
Err(err) => { Err(err) => {
self.error(err.to_string()); self.error(err.to_string());
return None; return None;
@ -416,7 +418,7 @@ impl<'a> Parser<'a> {
}; };
let start = self.parse_line_col()?; let start = self.parse_line_col()?;
let lines = Lines::from_bytes(text.as_ref()).expect("Errors shouldn't be annotated for files that aren't human readable (not valid utf-8)"); let lines = Lines::try_from(&bytes).expect("Errors shouldn't be annotated for files that aren't human readable (not valid utf-8)");
let range = if self.s.eat_if('-') { let range = if self.s.eat_if('-') {
let (line, col) = start; let (line, col) = start;
let start = lines.line_column_to_byte(line, col); let start = lines.line_column_to_byte(line, col);

View File

@ -95,7 +95,7 @@ impl TestWorld {
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::try_from(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");
} }