diff --git a/crates/typst-cli/src/fonts.rs b/crates/typst-cli/src/fonts.rs
index 1b4757c8b..3e89e0d6d 100644
--- a/crates/typst-cli/src/fonts.rs
+++ b/crates/typst-cli/src/fonts.rs
@@ -5,8 +5,8 @@ use std::path::{Path, PathBuf};
use memmap2::Mmap;
use typst::diag::StrResult;
+use typst::eval::Bytes;
use typst::font::{Font, FontBook, FontInfo, FontVariant};
-use typst::util::Bytes;
use walkdir::WalkDir;
use crate::args::FontsCommand;
diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs
index 06a728a6a..2c0ee7d06 100644
--- a/crates/typst-cli/src/world.rs
+++ b/crates/typst-cli/src/world.rs
@@ -9,10 +9,10 @@ use comemo::Prehashed;
use same_file::Handle;
use siphasher::sip128::{Hasher128, SipHasher13};
use typst::diag::{FileError, FileResult, StrResult};
-use typst::eval::{eco_format, Datetime, Library};
+use typst::eval::{eco_format, Bytes, Datetime, Library};
use typst::font::{Font, FontBook};
use typst::syntax::{FileId, Source};
-use typst::util::{Bytes, PathExt};
+use typst::util::PathExt;
use typst::World;
use crate::args::CompileCommand;
diff --git a/crates/typst-docs/src/html.rs b/crates/typst-docs/src/html.rs
index b021d4a79..38e56c093 100644
--- a/crates/typst-docs/src/html.rs
+++ b/crates/typst-docs/src/html.rs
@@ -4,11 +4,10 @@ use comemo::Prehashed;
use pulldown_cmark as md;
use typed_arena::Arena;
use typst::diag::FileResult;
-use typst::eval::{Datetime, Tracer};
+use typst::eval::{Bytes, Datetime, Tracer};
use typst::font::{Font, FontBook};
use typst::geom::{Point, Size};
use typst::syntax::{FileId, Source};
-use typst::util::Bytes;
use typst::World;
use yaml_front_matter::YamlFrontMatter;
diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs
index 4dda2c0d7..6d67ffad5 100644
--- a/crates/typst-docs/src/lib.rs
+++ b/crates/typst-docs/src/lib.rs
@@ -983,6 +983,7 @@ const TYPE_ORDER: &[&str] = &[
"color",
"datetime",
"string",
+ "bytes",
"regex",
"label",
"content",
diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs
index 1ce676bb4..4329f259c 100644
--- a/crates/typst-library/src/compute/construct.rs
+++ b/crates/typst-library/src/compute/construct.rs
@@ -3,7 +3,7 @@ use std::str::FromStr;
use time::{Month, PrimitiveDateTime};
-use typst::eval::{Datetime, Module, Regex};
+use typst::eval::{Bytes, Datetime, Module, Reflect, Regex};
use crate::prelude::*;
@@ -37,9 +37,9 @@ pub struct ToInt(i64);
cast! {
ToInt,
v: bool => Self(v as i64),
- v: i64 => Self(v),
v: f64 => Self(v as i64),
v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?),
+ v: i64 => Self(v),
}
/// Converts a value to a float.
@@ -77,9 +77,9 @@ cast! {
ToFloat,
v: bool => Self(v as i64 as f64),
v: i64 => Self(v as f64),
- v: f64 => Self(v),
v: Ratio => Self(v.get()),
v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?),
+ v: f64 => Self(v),
}
/// Creates a grayscale color.
@@ -486,6 +486,7 @@ cast! {
/// optional `base` parameter.
/// - Floats are formatted in base 10 and never in exponential notation.
/// - From labels the name is extracted.
+/// - Bytes are decoded as UTF-8.
///
/// If you wish to convert from and to Unicode code points, see
/// [`str.to-unicode`]($func/str.to-unicode) and
@@ -545,6 +546,11 @@ cast! {
v: i64 => Self::Int(v),
v: f64 => Self::Str(format_str!("{}", v)),
v: Label => Self::Str(v.0.into()),
+ v: Bytes => Self::Str(
+ std::str::from_utf8(&v)
+ .map_err(|_| "bytes are not valid utf-8")?
+ .into()
+ ),
v: Str => Self::Str(v),
}
@@ -633,35 +639,6 @@ cast! {
},
}
-/// Creates a label from a string.
-///
-/// Inserting a label into content attaches it to the closest previous element
-/// that is not a space. Then, the element can be [referenced]($func/ref) and
-/// styled through the label.
-///
-/// ## Example { #example }
-/// ```example
-/// #show : set text(blue)
-/// #show label("b"): set text(red)
-///
-/// = Heading
-/// *Strong* #label("b")
-/// ```
-///
-/// ## Syntax { #syntax }
-/// This function also has dedicated syntax: You can create a label by enclosing
-/// its name in angle brackets. This works both in markup and code.
-///
-/// Display: Label
-/// Category: construct
-#[func]
-pub fn label(
- /// The name of the label.
- name: EcoString,
-) -> Label {
- Label(name)
-}
-
/// Creates a regular expression from a string.
///
/// The result can be used as a
@@ -701,6 +678,106 @@ pub fn regex(
Regex::new(®ex.v).at(regex.span)
}
+/// Converts a value to bytes.
+///
+/// - Strings are encoded in UTF-8.
+/// - Arrays of integers between `{0}` and `{255}` are converted directly. The
+/// dedicated byte representation is much more efficient than the array
+/// representation and thus typically used for large byte buffers (e.g. image
+/// data).
+///
+/// ```example
+/// #bytes("Hello 😃") \
+/// #bytes((123, 160, 22, 0))
+/// ```
+///
+/// Display: Bytes
+/// Category: construct
+#[func]
+pub fn bytes(
+ /// The value that should be converted to a string.
+ value: ToBytes,
+) -> Bytes {
+ value.0
+}
+
+/// A value that can be cast to bytes.
+pub struct ToBytes(Bytes);
+
+cast! {
+ ToBytes,
+ v: Str => Self(v.as_bytes().into()),
+ v: Array => Self(v.iter()
+ .map(|v| match v {
+ Value::Int(byte @ 0..=255) => Ok(*byte as u8),
+ Value::Int(_) => bail!("number must be between 0 and 255"),
+ value => Err(::error(value)),
+ })
+ .collect::, _>>()?
+ .into()
+ ),
+ v: Bytes => Self(v),
+}
+
+/// Creates a label from a string.
+///
+/// Inserting a label into content attaches it to the closest previous element
+/// that is not a space. Then, the element can be [referenced]($func/ref) and
+/// styled through the label.
+///
+/// ## Example { #example }
+/// ```example
+/// #show : set text(blue)
+/// #show label("b"): set text(red)
+///
+/// = Heading
+/// *Strong* #label("b")
+/// ```
+///
+/// ## Syntax { #syntax }
+/// This function also has dedicated syntax: You can create a label by enclosing
+/// its name in angle brackets. This works both in markup and code.
+///
+/// Display: Label
+/// Category: construct
+#[func]
+pub fn label(
+ /// The name of the label.
+ name: EcoString,
+) -> Label {
+ Label(name)
+}
+
+/// Converts a value to an array.
+///
+/// Note that this function is only intended for conversion of a collection-like
+/// value to an array, not for creation of an array from individual items. Use
+/// the array syntax `(1, 2, 3)` (or `(1,)` for a single-element array) instead.
+///
+/// ```example
+/// #let hi = "Hello 😃"
+/// #array(bytes(hi))
+/// ```
+///
+/// Display: Array
+/// Category: construct
+#[func]
+pub fn array(
+ /// The value that should be converted to an array.
+ value: ToArray,
+) -> Array {
+ value.0
+}
+
+/// A value that can be cast to bytes.
+pub struct ToArray(Array);
+
+cast! {
+ ToArray,
+ v: Bytes => Self(v.iter().map(|&b| Value::Int(b as i64)).collect()),
+ v: Array => Self(v),
+}
+
/// Creates an array consisting of consecutive integers.
///
/// If you pass just one positional parameter, it is interpreted as the `end` of
diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs
index 6e3a298e1..4a7c53cc1 100644
--- a/crates/typst-library/src/compute/data.rs
+++ b/crates/typst-library/src/compute/data.rs
@@ -1,18 +1,24 @@
use typst::diag::{format_xml_like_error, FileError};
-use typst::eval::Datetime;
+use typst::eval::{Bytes, Datetime};
use crate::prelude::*;
-/// Reads plain text from a file.
+/// Reads plain text or data from a file.
///
-/// The file will be read and returned as a string.
+/// By default, the file will be read as UTF-8 and returned as a
+/// [string]($type/string).
+///
+/// If you specify `{encoding: none}`, this returns raw [bytes]($type/bytes)
+/// instead.
///
/// ## Example { #example }
/// ```example
+/// An example for a HTML file: \
/// #let text = read("data.html")
-///
-/// An example for a HTML file:\
/// #raw(text, lang: "html")
+///
+/// Raw bytes:
+/// #read("tiger.jpg", encoding: none)
/// ```
///
/// Display: Read
@@ -21,16 +27,52 @@ use crate::prelude::*;
pub fn read(
/// Path to a file.
path: Spanned,
+ /// The encoding to read the file with.
+ ///
+ /// If set to `{none}`, this function returns raw bytes.
+ #[named]
+ #[default(Some(Encoding::Utf8))]
+ encoding: Option,
/// The virtual machine.
vm: &mut Vm,
-) -> SourceResult {
+) -> SourceResult {
let Spanned { v: path, span } = path;
let id = vm.location().join(&path).at(span)?;
let data = vm.world().file(id).at(span)?;
- let text = std::str::from_utf8(&data)
- .map_err(|_| "file is not valid utf-8")
- .at(span)?;
- Ok(text.into())
+ Ok(match encoding {
+ None => Readable::Bytes(data),
+ Some(Encoding::Utf8) => Readable::Str(
+ std::str::from_utf8(&data)
+ .map_err(|_| "file is not valid utf-8")
+ .at(span)?
+ .into(),
+ ),
+ })
+}
+
+/// An encoding of a file.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum Encoding {
+ /// The Unicode UTF-8 encoding.
+ Utf8,
+}
+
+/// A value that can be read from a value.
+pub enum Readable {
+ /// A decoded string.
+ Str(Str),
+ /// Raw bytes.
+ Bytes(Bytes),
+}
+
+cast! {
+ Readable,
+ self => match self {
+ Self::Str(v) => v.into_value(),
+ Self::Bytes(v) => v.into_value(),
+ },
+ v: Str => Self::Str(v),
+ v: Bytes => Self::Bytes(v),
}
/// Reads structured data from a CSV file.
diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs
index 15309f129..757377f03 100644
--- a/crates/typst-library/src/compute/mod.rs
+++ b/crates/typst-library/src/compute/mod.rs
@@ -27,8 +27,10 @@ pub(super) fn define(global: &mut Scope) {
global.define("datetime", datetime_func());
global.define("symbol", symbol_func());
global.define("str", str_func());
+ global.define("bytes", bytes_func());
global.define("label", label_func());
global.define("regex", regex_func());
+ global.define("array", array_func());
global.define("range", range_func());
global.define("read", read_func());
global.define("csv", csv_func());
diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst-library/src/meta/bibliography.rs
index a2e7099d0..2b00ff445 100644
--- a/crates/typst-library/src/meta/bibliography.rs
+++ b/crates/typst-library/src/meta/bibliography.rs
@@ -8,7 +8,8 @@ use hayagriva::io::{BibLaTeXError, YamlBibliographyError};
use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting};
use hayagriva::Entry;
use typst::diag::FileError;
-use typst::util::{option_eq, Bytes};
+use typst::eval::Bytes;
+use typst::util::option_eq;
use super::{LinkElem, LocalName, RefElem};
use crate::layout::{BlockElem, GridElem, ParElem, Sizing, TrackSizings, VElem};
diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs
index 6713f5d0b..3d3fff408 100644
--- a/crates/typst-library/src/text/raw.rs
+++ b/crates/typst-library/src/text/raw.rs
@@ -6,8 +6,8 @@ use once_cell::unsync::Lazy as UnsyncLazy;
use syntect::highlighting as synt;
use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
use typst::diag::FileError;
+use typst::eval::Bytes;
use typst::syntax::{self, LinkedNode};
-use typst::util::Bytes;
use super::{
FontFamily, FontList, Hyphenate, LinebreakElem, SmartQuoteElem, TextElem, TextSize,
diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs
index e572f2ad7..514861e5d 100644
--- a/crates/typst-library/src/visualize/image.rs
+++ b/crates/typst-library/src/visualize/image.rs
@@ -1,8 +1,8 @@
use std::ffi::OsStr;
use std::path::Path;
+use typst::eval::Bytes;
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
-use typst::util::Bytes;
use crate::meta::{Figurable, LocalName};
use crate::prelude::*;
diff --git a/crates/typst/src/eval/array.rs b/crates/typst/src/eval/array.rs
index 86c41ff6c..adb3e858e 100644
--- a/crates/typst/src/eval/array.rs
+++ b/crates/typst/src/eval/array.rs
@@ -74,13 +74,9 @@ impl Array {
}
/// Borrow the value at the given index.
- pub fn at<'a>(
- &'a self,
- index: i64,
- default: Option<&'a Value>,
- ) -> StrResult<&'a Value> {
- self.locate(index)
- .and_then(|i| self.0.get(i))
+ pub fn at(&self, index: i64, default: Option) -> StrResult {
+ self.locate_opt(index, false)
+ .and_then(|i| self.0.get(i).cloned())
.or(default)
.ok_or_else(|| out_of_bounds_no_default(index, self.len()))
}
@@ -88,7 +84,7 @@ impl Array {
/// Mutably borrow the value at the given index.
pub fn at_mut(&mut self, index: i64) -> StrResult<&mut Value> {
let len = self.len();
- self.locate(index)
+ self.locate_opt(index, false)
.and_then(move |i| self.0.make_mut().get_mut(i))
.ok_or_else(|| out_of_bounds_no_default(index, len))
}
@@ -105,42 +101,21 @@ impl Array {
/// Insert a value at the specified index.
pub fn insert(&mut self, index: i64, value: Value) -> StrResult<()> {
- let len = self.len();
- let i = self
- .locate(index)
- .filter(|&i| i <= self.0.len())
- .ok_or_else(|| out_of_bounds(index, len))?;
-
+ let i = self.locate(index, true)?;
self.0.insert(i, value);
Ok(())
}
/// Remove and return the value at the specified index.
pub fn remove(&mut self, index: i64) -> StrResult {
- let len = self.len();
- let i = self
- .locate(index)
- .filter(|&i| i < self.0.len())
- .ok_or_else(|| out_of_bounds(index, len))?;
-
+ let i = self.locate(index, false)?;
Ok(self.0.remove(i))
}
/// Extract a contiguous subregion of the array.
pub fn slice(&self, start: i64, end: Option) -> StrResult {
- let len = self.len();
- let start = self
- .locate(start)
- .filter(|&start| start <= self.0.len())
- .ok_or_else(|| out_of_bounds(start, len))?;
-
- let end = end.unwrap_or(self.len() as i64);
- let end = self
- .locate(end)
- .filter(|&end| end <= self.0.len())
- .ok_or_else(|| out_of_bounds(end, len))?
- .max(start);
-
+ let start = self.locate(start, true)?;
+ let end = self.locate(end.unwrap_or(self.len() as i64), true)?.max(start);
Ok(self.0[start..end].into())
}
@@ -371,26 +346,6 @@ impl Array {
Ok(self.iter().cloned().cycle().take(count).collect())
}
- /// Extract a slice of the whole array.
- pub fn as_slice(&self) -> &[Value] {
- self.0.as_slice()
- }
-
- /// Iterate over references to the contained values.
- pub fn iter(&self) -> std::slice::Iter {
- self.0.iter()
- }
-
- /// Resolve an index.
- fn locate(&self, index: i64) -> Option {
- usize::try_from(if index >= 0 {
- index
- } else {
- (self.len() as i64).checked_add(index)?
- })
- .ok()
- }
-
/// Enumerate all items in the array.
pub fn enumerate(&self, start: i64) -> StrResult {
self.iter()
@@ -438,11 +393,44 @@ impl Array {
Ok(Self(out))
}
+
+ /// Extract a slice of the whole array.
+ pub fn as_slice(&self) -> &[Value] {
+ self.0.as_slice()
+ }
+
+ /// Iterate over references to the contained values.
+ pub fn iter(&self) -> std::slice::Iter {
+ self.0.iter()
+ }
+
+ /// Resolve an index or throw an out of bounds error.
+ fn locate(&self, index: i64, end_ok: bool) -> StrResult {
+ self.locate_opt(index, end_ok)
+ .ok_or_else(|| out_of_bounds(index, self.len()))
+ }
+
+ /// Resolve an index, if it is within bounds.
+ ///
+ /// `index == len` is considered in bounds if and only if `end_ok` is true.
+ fn locate_opt(&self, index: i64, end_ok: bool) -> Option {
+ let wrapped =
+ if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
+
+ wrapped
+ .and_then(|v| usize::try_from(v).ok())
+ .filter(|&v| v < self.0.len() + end_ok as usize)
+ }
}
impl Debug for Array {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- let pieces: Vec<_> = self.iter().map(|value| eco_format!("{value:?}")).collect();
+ let max = 40;
+ let mut pieces: Vec<_> =
+ self.iter().take(max).map(|value| eco_format!("{value:?}")).collect();
+ if self.len() > max {
+ pieces.push(eco_format!(".. ({} items omitted)", self.len() - max));
+ }
f.write_str(&pretty_array_like(&pieces, self.len() == 1))
}
}
diff --git a/crates/typst/src/eval/bytes.rs b/crates/typst/src/eval/bytes.rs
new file mode 100644
index 000000000..b24b289e3
--- /dev/null
+++ b/crates/typst/src/eval/bytes.rs
@@ -0,0 +1,111 @@
+use std::borrow::Cow;
+use std::fmt::{self, Debug, Formatter};
+use std::ops::Deref;
+use std::sync::Arc;
+
+use comemo::Prehashed;
+use ecow::{eco_format, EcoString};
+
+use crate::diag::StrResult;
+
+use super::Value;
+
+/// A shared byte buffer that is cheap to clone and hash.
+#[derive(Clone, Hash, Eq, PartialEq)]
+pub struct Bytes(Arc>>);
+
+impl Bytes {
+ /// Create a buffer from a static byte slice.
+ pub fn from_static(slice: &'static [u8]) -> Self {
+ Self(Arc::new(Prehashed::new(Cow::Borrowed(slice))))
+ }
+
+ /// Get the byte at the given index.
+ pub fn at(&self, index: i64, default: Option) -> StrResult {
+ self.locate_opt(index)
+ .and_then(|i| self.0.get(i).map(|&b| Value::Int(b as i64)))
+ .or(default)
+ .ok_or_else(|| out_of_bounds_no_default(index, self.len()))
+ }
+
+ /// Extract a contiguous subregion of the bytes.
+ pub fn slice(&self, start: i64, end: Option) -> StrResult {
+ let start = self.locate(start)?;
+ let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start);
+ Ok(self.0[start..end].into())
+ }
+
+ /// Return a view into the buffer.
+ pub fn as_slice(&self) -> &[u8] {
+ self
+ }
+
+ /// Return a copy of the buffer as a vector.
+ pub fn to_vec(&self) -> Vec {
+ self.0.to_vec()
+ }
+
+ /// Resolve an index or throw an out of bounds error.
+ fn locate(&self, index: i64) -> StrResult {
+ self.locate_opt(index).ok_or_else(|| out_of_bounds(index, self.len()))
+ }
+
+ /// Resolve an index, if it is within bounds.
+ ///
+ /// `index == len` is considered in bounds.
+ fn locate_opt(&self, index: i64) -> Option {
+ let wrapped =
+ if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
+
+ wrapped
+ .and_then(|v| usize::try_from(v).ok())
+ .filter(|&v| v <= self.0.len())
+ }
+}
+
+impl From<&[u8]> for Bytes {
+ fn from(slice: &[u8]) -> Self {
+ Self(Arc::new(Prehashed::new(slice.to_vec().into())))
+ }
+}
+
+impl From> for Bytes {
+ fn from(vec: Vec) -> Self {
+ Self(Arc::new(Prehashed::new(vec.into())))
+ }
+}
+
+impl Deref for Bytes {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl AsRef<[u8]> for Bytes {
+ fn as_ref(&self) -> &[u8] {
+ self
+ }
+}
+
+impl Debug for Bytes {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "bytes({})", self.len())
+ }
+}
+
+/// The out of bounds access error message.
+#[cold]
+fn out_of_bounds(index: i64, len: usize) -> EcoString {
+ eco_format!("byte index out of bounds (index: {index}, len: {len})")
+}
+
+/// The out of bounds access error message when no default value was given.
+#[cold]
+fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString {
+ eco_format!(
+ "byte index out of bounds (index: {index}, len: {len}) \
+ and no default value was specified",
+ )
+}
diff --git a/crates/typst/src/eval/dict.rs b/crates/typst/src/eval/dict.rs
index 3e6233aec..3b007c759 100644
--- a/crates/typst/src/eval/dict.rs
+++ b/crates/typst/src/eval/dict.rs
@@ -49,12 +49,12 @@ impl Dict {
}
/// Borrow the value the given `key` maps to,
- pub fn at<'a>(
- &'a self,
- key: &str,
- default: Option<&'a Value>,
- ) -> StrResult<&'a Value> {
- self.0.get(key).or(default).ok_or_else(|| missing_key_no_default(key))
+ pub fn at(&self, key: &str, default: Option) -> StrResult {
+ self.0
+ .get(key)
+ .cloned()
+ .or(default)
+ .ok_or_else(|| missing_key_no_default(key))
}
/// Mutably borrow the value the given `key` maps to.
@@ -140,8 +140,10 @@ impl Debug for Dict {
return f.write_str("(:)");
}
- let pieces: Vec<_> = self
+ let max = 40;
+ let mut pieces: Vec<_> = self
.iter()
+ .take(max)
.map(|(key, value)| {
if is_ident(key) {
eco_format!("{key}: {value:?}")
@@ -151,6 +153,10 @@ impl Debug for Dict {
})
.collect();
+ if self.len() > max {
+ pieces.push(eco_format!(".. ({} pairs omitted)", self.len() - max));
+ }
+
f.write_str(&pretty_array_like(&pieces, false))
}
}
diff --git a/crates/typst/src/eval/methods.rs b/crates/typst/src/eval/methods.rs
index 6a846f524..0247a4a73 100644
--- a/crates/typst/src/eval/methods.rs
+++ b/crates/typst/src/eval/methods.rs
@@ -55,11 +55,10 @@ pub fn call(
"len" => string.len().into_value(),
"first" => string.first().at(span)?.into_value(),
"last" => string.last().at(span)?.into_value(),
- "at" => {
- let index = args.expect("index")?;
- let default = args.named::("default")?;
- string.at(index, default.as_deref()).at(span)?.into_value()
- }
+ "at" => string
+ .at(args.expect("index")?, args.named("default")?)
+ .at(span)?
+ .into_value(),
"slice" => {
let start = args.expect("start")?;
let mut end = args.eat()?;
@@ -93,11 +92,25 @@ pub fn call(
_ => return missing(),
},
+ Value::Bytes(bytes) => match method {
+ "len" => bytes.len().into_value(),
+ "at" => bytes.at(args.expect("index")?, args.named("default")?).at(span)?,
+ "slice" => {
+ let start = args.expect("start")?;
+ let mut end = args.eat()?;
+ if end.is_none() {
+ end = args.named("count")?.map(|c: i64| start + c);
+ }
+ bytes.slice(start, end).at(span)?.into_value()
+ }
+ _ => return missing(),
+ },
+
Value::Content(content) => match method {
"func" => content.func().into_value(),
"has" => content.has(&args.expect::("field")?).into_value(),
"at" => content
- .at(&args.expect::("field")?, args.named("default")?)
+ .at(&args.expect::("field")?, args.named("default")?)
.at(span)?,
"fields" => content.dict().into_value(),
"location" => content
@@ -112,10 +125,7 @@ pub fn call(
"len" => array.len().into_value(),
"first" => array.first().at(span)?.clone(),
"last" => array.last().at(span)?.clone(),
- "at" => array
- .at(args.expect("index")?, args.named("default")?.as_ref())
- .at(span)?
- .clone(),
+ "at" => array.at(args.expect("index")?, args.named("default")?).at(span)?,
"slice" => {
let start = args.expect("start")?;
let mut end = args.eat()?;
@@ -157,9 +167,8 @@ pub fn call(
Value::Dict(dict) => match method {
"len" => dict.len().into_value(),
"at" => dict
- .at(&args.expect::("key")?, args.named("default")?.as_ref())
- .at(span)?
- .clone(),
+ .at(&args.expect::("key")?, args.named("default")?)
+ .at(span)?,
"keys" => dict.keys().into_value(),
"values" => dict.values().into_value(),
"pairs" => dict.pairs().into_value(),
@@ -396,6 +405,7 @@ pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
("starts-with", true),
("trim", true),
],
+ "bytes" => &[("len", false), ("at", true), ("slice", true)],
"content" => &[
("func", false),
("has", true),
diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs
index 15f0fdddd..770d9fd1c 100644
--- a/crates/typst/src/eval/mod.rs
+++ b/crates/typst/src/eval/mod.rs
@@ -14,6 +14,7 @@ mod str;
mod value;
mod args;
mod auto;
+mod bytes;
mod datetime;
mod fields;
mod func;
@@ -40,6 +41,7 @@ pub use typst_macros::{func, symbols};
pub use self::args::{Arg, Args};
pub use self::array::{array, Array};
pub use self::auto::AutoValue;
+pub use self::bytes::Bytes;
pub use self::cast::{
cast, Cast, CastInfo, FromValue, IntoResult, IntoValue, Never, Reflect, Variadics,
};
@@ -1371,7 +1373,7 @@ where
let Ok(v) = value.at(i as i64, None) else {
bail!(expr.span(), "not enough elements to destructure");
};
- f(vm, expr, v.clone())?;
+ f(vm, expr, v)?;
i += 1;
}
ast::DestructuringKind::Sink(spread) => {
@@ -1423,7 +1425,7 @@ where
.at(&ident, None)
.map_err(|_| "destructuring key not found in dictionary")
.at(ident.span())?;
- f(vm, ast::Expr::Ident(ident.clone()), v.clone())?;
+ f(vm, ast::Expr::Ident(ident.clone()), v)?;
used.insert(ident.take());
}
ast::DestructuringKind::Sink(spread) => sink = spread.expr(),
@@ -1433,7 +1435,7 @@ where
.at(&name, None)
.map_err(|_| "destructuring key not found in dictionary")
.at(name.span())?;
- f(vm, named.expr(), v.clone())?;
+ f(vm, named.expr(), v)?;
used.insert(name.take());
}
ast::DestructuringKind::Placeholder(_) => {}
diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs
index 0880a87e9..323175995 100644
--- a/crates/typst/src/eval/ops.rs
+++ b/crates/typst/src/eval/ops.rs
@@ -347,6 +347,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Color(a), Color(b)) => a == b,
(Symbol(a), Symbol(b)) => a == b,
(Str(a), Str(b)) => a == b,
+ (Bytes(a), Bytes(b)) => a == b,
(Label(a), Label(b)) => a == b,
(Content(a), Content(b)) => a == b,
(Array(a), Array(b)) => a == b,
diff --git a/crates/typst/src/eval/str.rs b/crates/typst/src/eval/str.rs
index f5e5ab00a..1d88b81b5 100644
--- a/crates/typst/src/eval/str.rs
+++ b/crates/typst/src/eval/str.rs
@@ -68,14 +68,12 @@ impl Str {
}
/// Extract the grapheme cluster at the given index.
- pub fn at<'a>(&'a self, index: i64, default: Option<&'a str>) -> StrResult {
+ pub fn at(&self, index: i64, default: Option) -> StrResult {
let len = self.len();
- let grapheme = self
- .locate_opt(index)?
- .and_then(|i| self.0[i..].graphemes(true).next())
+ self.locate_opt(index)?
+ .and_then(|i| self.0[i..].graphemes(true).next().map(|s| s.into_value()))
.or(default)
- .ok_or_else(|| no_default_and_out_of_bounds(index, len))?;
- Ok(grapheme.into())
+ .ok_or_else(|| no_default_and_out_of_bounds(index, len))
}
/// Extract a contiguous substring.
@@ -324,8 +322,15 @@ impl Str {
Ok(Self(self.0.repeat(n)))
}
- /// Resolve an index, if it is within bounds.
- /// Errors on invalid char boundaries.
+ /// Resolve an index or throw an out of bounds error.
+ fn locate(&self, index: i64) -> StrResult {
+ self.locate_opt(index)?
+ .ok_or_else(|| out_of_bounds(index, self.len()))
+ }
+
+ /// Resolve an index, if it is within bounds and on a valid char boundary.
+ ///
+ /// `index == len` is considered in bounds.
fn locate_opt(&self, index: i64) -> StrResult