From ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 2 Jul 2023 19:59:52 +0200 Subject: Move everything into `crates/` directory --- src/diag.rs | 376 --------- src/doc.rs | 719 ---------------- src/eval/args.rs | 216 ----- src/eval/array.rs | 508 ------------ src/eval/auto.rs | 39 - src/eval/cast.rs | 316 ------- src/eval/datetime.rs | 201 ----- src/eval/dict.rs | 235 ------ src/eval/func.rs | 643 --------------- src/eval/int.rs | 81 -- src/eval/library.rs | 182 ----- src/eval/methods.rs | 373 --------- src/eval/mod.rs | 1908 ------------------------------------------- src/eval/module.rs | 98 --- src/eval/none.rs | 74 -- src/eval/ops.rs | 429 ---------- src/eval/scope.rs | 178 ---- src/eval/str.rs | 620 -------------- src/eval/symbol.rs | 210 ----- src/eval/value.rs | 461 ----------- src/export/mod.rs | 7 - src/export/pdf/font.rs | 204 ----- src/export/pdf/image.rs | 143 ---- src/export/pdf/mod.rs | 235 ------ src/export/pdf/outline.rs | 127 --- src/export/pdf/page.rs | 565 ------------- src/export/render.rs | 673 --------------- src/file.rs | 303 ------- src/font/book.rs | 546 ------------- src/font/mod.rs | 249 ------ src/font/variant.rs | 270 ------ src/geom/abs.rs | 266 ------ src/geom/align.rs | 239 ------ src/geom/angle.rs | 188 ----- src/geom/axes.rs | 305 ------- src/geom/color.rs | 386 --------- src/geom/corners.rs | 219 ----- src/geom/dir.rs | 79 -- src/geom/ellipse.rs | 22 - src/geom/em.rs | 153 ---- src/geom/fr.rs | 119 --- src/geom/length.rs | 128 --- src/geom/macros.rs | 47 -- src/geom/mod.rs | 121 --- src/geom/paint.rs | 30 - src/geom/path.rs | 54 -- src/geom/point.rs | 146 ---- src/geom/ratio.rs | 133 --- src/geom/rel.rs | 246 ------ src/geom/rounded.rs | 182 ----- src/geom/scalar.rs | 175 ---- src/geom/shape.rs | 35 - src/geom/sides.rs | 268 ------ src/geom/size.rs | 78 -- src/geom/smart.rs | 146 ---- src/geom/stroke.rs | 387 --------- src/geom/transform.rs | 77 -- src/ide/analyze.rs | 111 --- src/ide/complete.rs | 1201 --------------------------- src/ide/highlight.rs | 430 ---------- src/ide/jump.rs | 173 ---- src/ide/mod.rs | 97 --- src/ide/tooltip.rs | 222 ----- src/image.rs | 449 ---------- src/lib.rs | 147 ---- src/model/content.rs | 614 -------------- src/model/element.rs | 134 --- src/model/introspect.rs | 352 -------- src/model/label.rs | 16 - src/model/mod.rs | 148 ---- src/model/realize.rs | 228 ------ src/model/selector.rs | 296 ------- src/model/styles.rs | 750 ----------------- src/syntax/ast.rs | 1994 --------------------------------------------- src/syntax/kind.rs | 448 ---------- src/syntax/lexer.rs | 738 ----------------- src/syntax/mod.rs | 23 - src/syntax/node.rs | 889 -------------------- src/syntax/parser.rs | 1643 ------------------------------------- src/syntax/reparser.rs | 322 -------- src/syntax/source.rs | 421 ---------- src/syntax/span.rs | 148 ---- src/util/bytes.rs | 59 -- src/util/fat.rs | 55 -- src/util/mod.rs | 268 ------ 85 files changed, 27794 deletions(-) delete mode 100644 src/diag.rs delete mode 100644 src/doc.rs delete mode 100644 src/eval/args.rs delete mode 100644 src/eval/array.rs delete mode 100644 src/eval/auto.rs delete mode 100644 src/eval/cast.rs delete mode 100644 src/eval/datetime.rs delete mode 100644 src/eval/dict.rs delete mode 100644 src/eval/func.rs delete mode 100644 src/eval/int.rs delete mode 100644 src/eval/library.rs delete mode 100644 src/eval/methods.rs delete mode 100644 src/eval/mod.rs delete mode 100644 src/eval/module.rs delete mode 100644 src/eval/none.rs delete mode 100644 src/eval/ops.rs delete mode 100644 src/eval/scope.rs delete mode 100644 src/eval/str.rs delete mode 100644 src/eval/symbol.rs delete mode 100644 src/eval/value.rs delete mode 100644 src/export/mod.rs delete mode 100644 src/export/pdf/font.rs delete mode 100644 src/export/pdf/image.rs delete mode 100644 src/export/pdf/mod.rs delete mode 100644 src/export/pdf/outline.rs delete mode 100644 src/export/pdf/page.rs delete mode 100644 src/export/render.rs delete mode 100644 src/file.rs delete mode 100644 src/font/book.rs delete mode 100644 src/font/mod.rs delete mode 100644 src/font/variant.rs delete mode 100644 src/geom/abs.rs delete mode 100644 src/geom/align.rs delete mode 100644 src/geom/angle.rs delete mode 100644 src/geom/axes.rs delete mode 100644 src/geom/color.rs delete mode 100644 src/geom/corners.rs delete mode 100644 src/geom/dir.rs delete mode 100644 src/geom/ellipse.rs delete mode 100644 src/geom/em.rs delete mode 100644 src/geom/fr.rs delete mode 100644 src/geom/length.rs delete mode 100644 src/geom/macros.rs delete mode 100644 src/geom/mod.rs delete mode 100644 src/geom/paint.rs delete mode 100644 src/geom/path.rs delete mode 100644 src/geom/point.rs delete mode 100644 src/geom/ratio.rs delete mode 100644 src/geom/rel.rs delete mode 100644 src/geom/rounded.rs delete mode 100644 src/geom/scalar.rs delete mode 100644 src/geom/shape.rs delete mode 100644 src/geom/sides.rs delete mode 100644 src/geom/size.rs delete mode 100644 src/geom/smart.rs delete mode 100644 src/geom/stroke.rs delete mode 100644 src/geom/transform.rs delete mode 100644 src/ide/analyze.rs delete mode 100644 src/ide/complete.rs delete mode 100644 src/ide/highlight.rs delete mode 100644 src/ide/jump.rs delete mode 100644 src/ide/mod.rs delete mode 100644 src/ide/tooltip.rs delete mode 100644 src/image.rs delete mode 100644 src/lib.rs delete mode 100644 src/model/content.rs delete mode 100644 src/model/element.rs delete mode 100644 src/model/introspect.rs delete mode 100644 src/model/label.rs delete mode 100644 src/model/mod.rs delete mode 100644 src/model/realize.rs delete mode 100644 src/model/selector.rs delete mode 100644 src/model/styles.rs delete mode 100644 src/syntax/ast.rs delete mode 100644 src/syntax/kind.rs delete mode 100644 src/syntax/lexer.rs delete mode 100644 src/syntax/mod.rs delete mode 100644 src/syntax/node.rs delete mode 100644 src/syntax/parser.rs delete mode 100644 src/syntax/reparser.rs delete mode 100644 src/syntax/source.rs delete mode 100644 src/syntax/span.rs delete mode 100644 src/util/bytes.rs delete mode 100644 src/util/fat.rs delete mode 100644 src/util/mod.rs (limited to 'src') diff --git a/src/diag.rs b/src/diag.rs deleted file mode 100644 index b5995be4..00000000 --- a/src/diag.rs +++ /dev/null @@ -1,376 +0,0 @@ -//! Diagnostics. - -use std::fmt::{self, Display, Formatter}; -use std::io; -use std::path::{Path, PathBuf}; -use std::str::Utf8Error; -use std::string::FromUtf8Error; - -use comemo::Tracked; - -use crate::file::PackageSpec; -use crate::syntax::{Span, Spanned}; -use crate::World; - -/// Early-return with a [`StrResult`] or [`SourceResult`]. -/// -/// If called with just a string and format args, returns with a -/// `StrResult`. If called with a span, a string and format args, returns -/// a `SourceResult`. -/// -/// ``` -/// bail!("bailing with a {}", "string result"); -/// bail!(span, "bailing with a {}", "source result"); -/// ``` -#[macro_export] -#[doc(hidden)] -macro_rules! __bail { - ($fmt:literal $(, $arg:expr)* $(,)?) => { - return Err($crate::diag::eco_format!($fmt, $($arg),*)) - }; - - ($error:expr) => { - return Err(Box::new(vec![$error])) - }; - - ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { - return Err(Box::new(vec![$crate::diag::SourceError::new( - $span, - $crate::diag::eco_format!($fmt, $($arg),*), - )])) - }; -} - -#[doc(inline)] -pub use crate::__bail as bail; - -/// Construct an [`EcoString`] or [`SourceError`]. -#[macro_export] -#[doc(hidden)] -macro_rules! __error { - ($fmt:literal $(, $arg:expr)* $(,)?) => { - $crate::diag::eco_format!($fmt, $($arg),*) - }; - - ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { - $crate::diag::SourceError::new( - $span, - $crate::diag::eco_format!($fmt, $($arg),*), - ) - }; -} - -#[doc(inline)] -pub use crate::__error as error; -#[doc(hidden)] -pub use ecow::{eco_format, EcoString}; - -/// A result that can carry multiple source errors. -pub type SourceResult = Result>>; - -/// An error in a source file. -/// -/// The contained spans will only be detached if any of the input source files -/// were detached. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct SourceError { - /// The span of the erroneous node in the source code. - pub span: Span, - /// A diagnostic message describing the problem. - pub message: EcoString, - /// The trace of function calls leading to the error. - pub trace: Vec>, - /// Additonal hints to the user, indicating how this error could be avoided - /// or worked around. - pub hints: Vec, -} - -impl SourceError { - /// Create a new, bare error. - pub fn new(span: Span, message: impl Into) -> Self { - Self { - span, - trace: vec![], - message: message.into(), - hints: vec![], - } - } - - /// Adds user-facing hints to the error. - pub fn with_hints(mut self, hints: impl IntoIterator) -> Self { - self.hints.extend(hints); - self - } -} - -/// A part of an error's [trace](SourceError::trace). -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum Tracepoint { - /// A function call. - Call(Option), - /// A show rule application. - Show(EcoString), - /// A module import. - Import, -} - -impl Display for Tracepoint { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Tracepoint::Call(Some(name)) => { - write!(f, "error occurred in this call of function `{}`", name) - } - Tracepoint::Call(None) => { - write!(f, "error occurred in this function call") - } - Tracepoint::Show(name) => { - write!(f, "error occurred while applying show rule to this {name}") - } - Tracepoint::Import => { - write!(f, "error occurred while importing this module") - } - } - } -} - -/// Enrich a [`SourceResult`] with a tracepoint. -pub trait Trace { - /// Add the tracepoint to all errors that lie outside the `span`. - fn trace(self, world: Tracked, make_point: F, span: Span) -> Self - where - F: Fn() -> Tracepoint; -} - -impl Trace for SourceResult { - fn trace(self, world: Tracked, make_point: F, span: Span) -> Self - where - F: Fn() -> Tracepoint, - { - self.map_err(|mut errors| { - if span.is_detached() { - return errors; - } - - let trace_range = span.range(&*world); - for error in errors.iter_mut().filter(|e| !e.span.is_detached()) { - // Skip traces that surround the error. - if error.span.id() == span.id() { - let error_range = error.span.range(&*world); - if trace_range.start <= error_range.start - && trace_range.end >= error_range.end - { - continue; - } - } - - error.trace.push(Spanned::new(make_point(), span)); - } - errors - }) - } -} - -/// A result type with a string error message. -pub type StrResult = Result; - -/// Convert a [`StrResult`] to a [`SourceResult`] by adding span information. -pub trait At { - /// Add the span information. - fn at(self, span: Span) -> SourceResult; -} - -impl At for Result -where - S: Into, -{ - fn at(self, span: Span) -> SourceResult { - self.map_err(|message| Box::new(vec![SourceError::new(span, message)])) - } -} - -/// A result type with a string error message and hints. -pub type HintedStrResult = Result; - -/// A string message with hints. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct HintedString { - /// A diagnostic message describing the problem. - pub message: EcoString, - /// Additonal hints to the user, indicating how this error could be avoided - /// or worked around. - pub hints: Vec, -} - -impl At for Result { - fn at(self, span: Span) -> SourceResult { - self.map_err(|diags| { - Box::new(vec![SourceError::new(span, diags.message).with_hints(diags.hints)]) - }) - } -} - -/// Enrich a [`StrResult`] or [`HintedStrResult`] with a hint. -pub trait Hint { - /// Add the hint. - fn hint(self, hint: impl Into) -> HintedStrResult; -} - -impl Hint for StrResult { - fn hint(self, hint: impl Into) -> HintedStrResult { - self.map_err(|message| HintedString { message, hints: vec![hint.into()] }) - } -} - -impl Hint for HintedStrResult { - fn hint(self, hint: impl Into) -> HintedStrResult { - self.map_err(|mut error| { - error.hints.push(hint.into()); - error - }) - } -} - -/// A result type with a file-related error. -pub type FileResult = Result; - -/// An error that occurred while trying to load of a file. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum FileError { - /// A file was not found at this path. - NotFound(PathBuf), - /// A file could not be accessed. - AccessDenied, - /// A directory was found, but a file was expected. - IsDirectory, - /// The file is not a Typst source file, but should have been. - NotSource, - /// The file was not valid UTF-8, but should have been. - InvalidUtf8, - /// The package the file is part of could not be loaded. - Package(PackageError), - /// Another error. - Other, -} - -impl FileError { - /// Create a file error from an I/O error. - pub fn from_io(error: io::Error, path: &Path) -> Self { - match error.kind() { - io::ErrorKind::NotFound => Self::NotFound(path.into()), - io::ErrorKind::PermissionDenied => Self::AccessDenied, - io::ErrorKind::InvalidData - if error.to_string().contains("stream did not contain valid UTF-8") => - { - Self::InvalidUtf8 - } - _ => Self::Other, - } - } -} - -impl std::error::Error for FileError {} - -impl Display for FileError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::NotFound(path) => { - write!(f, "file not found (searched at {})", path.display()) - } - Self::AccessDenied => f.pad("failed to load file (access denied)"), - Self::IsDirectory => f.pad("failed to load file (is a directory)"), - Self::NotSource => f.pad("not a typst source file"), - Self::InvalidUtf8 => f.pad("file is not valid utf-8"), - Self::Package(error) => error.fmt(f), - Self::Other => f.pad("failed to load file"), - } - } -} - -impl From for FileError { - fn from(_: Utf8Error) -> Self { - Self::InvalidUtf8 - } -} - -impl From for FileError { - fn from(_: FromUtf8Error) -> Self { - Self::InvalidUtf8 - } -} - -impl From for FileError { - fn from(error: PackageError) -> Self { - Self::Package(error) - } -} - -impl From for EcoString { - fn from(error: FileError) -> Self { - eco_format!("{error}") - } -} - -/// A result type with a package-related error. -pub type PackageResult = Result; - -/// An error that occured while trying to load a package. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum PackageError { - /// The specified package does not exist. - NotFound(PackageSpec), - /// Failed to retrieve the package through the network. - NetworkFailed, - /// The package archive was malformed. - MalformedArchive, - /// Another error. - Other, -} - -impl std::error::Error for PackageError {} - -impl Display for PackageError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::NotFound(spec) => { - write!(f, "package not found (searched for {spec})",) - } - Self::NetworkFailed => f.pad("failed to load package (network failed)"), - Self::MalformedArchive => f.pad("failed to load package (archive malformed)"), - Self::Other => f.pad("failed to load package"), - } - } -} - -impl From for EcoString { - fn from(error: PackageError) -> Self { - eco_format!("{error}") - } -} -/// 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 { - roxmltree::Error::UnexpectedCloseTag { expected, actual, pos } => { - eco_format!( - "failed to parse {format}: found closing tag '{actual}' \ - instead of '{expected}' in line {}", - pos.row - ) - } - roxmltree::Error::UnknownEntityReference(entity, pos) => { - eco_format!( - "failed to parse {format}: unknown entity '{entity}' in line {}", - pos.row - ) - } - roxmltree::Error::DuplicatedAttribute(attr, pos) => { - eco_format!( - "failed to parse {format}: duplicate attribute '{attr}' in line {}", - pos.row - ) - } - roxmltree::Error::NoRootNode => { - eco_format!("failed to parse {format}: missing root node") - } - _ => eco_format!("failed to parse {format}"), - } -} diff --git a/src/doc.rs b/src/doc.rs deleted file mode 100644 index de16cece..00000000 --- a/src/doc.rs +++ /dev/null @@ -1,719 +0,0 @@ -//! Finished documents. - -use std::fmt::{self, Debug, Formatter}; -use std::num::NonZeroUsize; -use std::ops::Range; -use std::str::FromStr; -use std::sync::Arc; - -use ecow::EcoString; - -use crate::eval::{cast, dict, Dict, Value}; -use crate::font::Font; -use crate::geom::{ - self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length, - Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, -}; -use crate::image::Image; -use crate::model::{Content, Location, MetaElem, StyleChain}; -use crate::syntax::Span; - -/// A finished document with metadata and page frames. -#[derive(Debug, Default, Clone, Hash)] -pub struct Document { - /// The page frames. - pub pages: Vec, - /// The document's title. - pub title: Option, - /// The document's author. - pub author: Vec, -} - -/// A finished layout with items at fixed positions. -#[derive(Default, Clone, Hash)] -pub struct Frame { - /// The size of the frame. - size: Size, - /// The baseline of the frame measured from the top. If this is `None`, the - /// frame's implicit baseline is at the bottom. - baseline: Option, - /// The items composing this layout. - items: Arc>, -} - -/// Constructor, accessors and setters. -impl Frame { - /// Create a new, empty frame. - /// - /// Panics the size is not finite. - #[track_caller] - pub fn new(size: Size) -> Self { - assert!(size.is_finite()); - Self { size, baseline: None, items: Arc::new(vec![]) } - } - - /// Whether the frame contains no items. - pub fn is_empty(&self) -> bool { - self.items.is_empty() - } - - /// The size of the frame. - pub fn size(&self) -> Size { - self.size - } - - /// The size of the frame, mutably. - pub fn size_mut(&mut self) -> &mut Size { - &mut self.size - } - - /// Set the size of the frame. - pub fn set_size(&mut self, size: Size) { - self.size = size; - } - - /// The width of the frame. - pub fn width(&self) -> Abs { - self.size.x - } - - /// The height of the frame. - pub fn height(&self) -> Abs { - self.size.y - } - - /// The vertical position of the frame's baseline. - pub fn baseline(&self) -> Abs { - self.baseline.unwrap_or(self.size.y) - } - - /// Whether the frame has a non-default baseline. - pub fn has_baseline(&self) -> bool { - self.baseline.is_some() - } - - /// Set the frame's baseline from the top. - pub fn set_baseline(&mut self, baseline: Abs) { - self.baseline = Some(baseline); - } - - /// The distance from the baseline to the top of the frame. - /// - /// This is the same as `baseline()`, but more in line with the terminology - /// used in math layout. - pub fn ascent(&self) -> Abs { - self.baseline() - } - - /// The distance from the baseline to the bottom of the frame. - pub fn descent(&self) -> Abs { - self.size.y - self.baseline() - } - - /// An iterator over the items inside this frame alongside their positions - /// relative to the top-left of the frame. - pub fn items(&self) -> std::slice::Iter<'_, (Point, FrameItem)> { - self.items.iter() - } -} - -/// Insert items and subframes. -impl Frame { - /// The layer the next item will be added on. This corresponds to the number - /// of items in the frame. - pub fn layer(&self) -> usize { - self.items.len() - } - - /// Add an item at a position in the foreground. - pub fn push(&mut self, pos: Point, item: FrameItem) { - Arc::make_mut(&mut self.items).push((pos, item)); - } - - /// Add a frame at a position in the foreground. - /// - /// Automatically decides whether to inline the frame or to include it as a - /// group based on the number of items in it. - pub fn push_frame(&mut self, pos: Point, frame: Frame) { - if self.should_inline(&frame) { - self.inline(self.layer(), pos, frame); - } else { - self.push(pos, FrameItem::Group(GroupItem::new(frame))); - } - } - - /// Insert an item at the given layer in the frame. - /// - /// This panics if the layer is greater than the number of layers present. - #[track_caller] - pub fn insert(&mut self, layer: usize, pos: Point, items: FrameItem) { - Arc::make_mut(&mut self.items).insert(layer, (pos, items)); - } - - /// Add an item at a position in the background. - pub fn prepend(&mut self, pos: Point, item: FrameItem) { - Arc::make_mut(&mut self.items).insert(0, (pos, item)); - } - - /// Add multiple items at a position in the background. - /// - /// The first item in the iterator will be the one that is most in the - /// background. - pub fn prepend_multiple(&mut self, items: I) - where - I: IntoIterator, - { - Arc::make_mut(&mut self.items).splice(0..0, items); - } - - /// Add a frame at a position in the background. - pub fn prepend_frame(&mut self, pos: Point, frame: Frame) { - if self.should_inline(&frame) { - self.inline(0, pos, frame); - } else { - self.prepend(pos, FrameItem::Group(GroupItem::new(frame))); - } - } - - /// Whether the given frame should be inlined. - fn should_inline(&self, frame: &Frame) -> bool { - self.items.is_empty() || frame.items.len() <= 5 - } - - /// Inline a frame at the given layer. - fn inline(&mut self, layer: usize, pos: Point, frame: Frame) { - // Try to just reuse the items. - if pos.is_zero() && self.items.is_empty() { - self.items = frame.items; - return; - } - - // Try to transfer the items without adjusting the position. - // Also try to reuse the items if the Arc isn't shared. - let range = layer..layer; - if pos.is_zero() { - let sink = Arc::make_mut(&mut self.items); - match Arc::try_unwrap(frame.items) { - Ok(items) => { - sink.splice(range, items); - } - Err(arc) => { - sink.splice(range, arc.iter().cloned()); - } - } - return; - } - - // We have to adjust the item positions. - // But still try to reuse the items if the Arc isn't shared. - let sink = Arc::make_mut(&mut self.items); - match Arc::try_unwrap(frame.items) { - Ok(items) => { - sink.splice(range, items.into_iter().map(|(p, e)| (p + pos, e))); - } - Err(arc) => { - sink.splice(range, arc.iter().cloned().map(|(p, e)| (p + pos, e))); - } - } - } -} - -/// Modify the frame. -impl Frame { - /// Remove all items from the frame. - pub fn clear(&mut self) { - if Arc::strong_count(&self.items) == 1 { - Arc::make_mut(&mut self.items).clear(); - } else { - self.items = Arc::new(vec![]); - } - } - - /// Resize the frame to a new size, distributing new space according to the - /// given alignments. - pub fn resize(&mut self, target: Size, aligns: Axes) { - if self.size != target { - let offset = Point::new( - aligns.x.position(target.x - self.size.x), - aligns.y.position(target.y - self.size.y), - ); - self.size = target; - self.translate(offset); - } - } - - /// Move the baseline and contents of the frame by an offset. - pub fn translate(&mut self, offset: Point) { - if !offset.is_zero() { - if let Some(baseline) = &mut self.baseline { - *baseline += offset.y; - } - for (point, _) in Arc::make_mut(&mut self.items) { - *point += offset; - } - } - } - - /// Attach the metadata from this style chain to the frame. - pub fn meta(&mut self, styles: StyleChain, force: bool) { - if force || !self.is_empty() { - self.meta_iter(MetaElem::data_in(styles)); - } - } - - /// Attach metadata from an iterator. - pub fn meta_iter(&mut self, iter: impl IntoIterator) { - let mut hide = false; - for meta in iter { - if matches!(meta, Meta::Hide) { - hide = true; - } else { - self.prepend(Point::zero(), FrameItem::Meta(meta, self.size)); - } - } - if hide { - Arc::make_mut(&mut self.items).retain(|(_, item)| { - matches!(item, FrameItem::Group(_) | FrameItem::Meta(Meta::Elem(_), _)) - }); - } - } - - /// Add a background fill. - pub fn fill(&mut self, fill: Paint) { - self.prepend( - Point::zero(), - FrameItem::Shape(Geometry::Rect(self.size()).filled(fill), Span::detached()), - ); - } - - /// Add a fill and stroke with optional radius and outset to the frame. - pub fn fill_and_stroke( - &mut self, - fill: Option, - stroke: Sides>, - outset: Sides>, - radius: Corners>, - span: Span, - ) { - let outset = outset.relative_to(self.size()); - let size = self.size() + outset.sum_by_axis(); - let pos = Point::new(-outset.left, -outset.top); - let radius = radius.map(|side| side.relative_to(size.x.min(size.y) / 2.0)); - self.prepend_multiple( - rounded_rect(size, radius, fill, stroke) - .into_iter() - .map(|x| (pos, FrameItem::Shape(x, span))), - ) - } - - /// Arbitrarily transform the contents of the frame. - pub fn transform(&mut self, transform: Transform) { - if !self.is_empty() { - self.group(|g| g.transform = transform); - } - } - - /// Clip the contents of a frame to its size. - pub fn clip(&mut self) { - if !self.is_empty() { - self.group(|g| g.clips = true); - } - } - - /// Wrap the frame's contents in a group and modify that group with `f`. - fn group(&mut self, f: F) - where - F: FnOnce(&mut GroupItem), - { - let mut wrapper = Frame::new(self.size); - wrapper.baseline = self.baseline; - let mut group = GroupItem::new(std::mem::take(self)); - f(&mut group); - wrapper.push(Point::zero(), FrameItem::Group(group)); - *self = wrapper; - } -} - -/// Tools for debugging. -impl Frame { - /// Add a full size aqua background and a red baseline for debugging. - pub fn debug(mut self) -> Self { - self.insert( - 0, - Point::zero(), - FrameItem::Shape( - Geometry::Rect(self.size) - .filled(RgbaColor { a: 100, ..Color::TEAL.to_rgba() }.into()), - Span::detached(), - ), - ); - self.insert( - 1, - Point::with_y(self.baseline()), - FrameItem::Shape( - Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { - paint: Color::RED.into(), - thickness: Abs::pt(1.0), - ..Stroke::default() - }), - Span::detached(), - ), - ); - self - } - - /// Add a green marker at a position for debugging. - pub fn mark_point(&mut self, pos: Point) { - let radius = Abs::pt(2.0); - self.push( - pos - Point::splat(radius), - FrameItem::Shape( - geom::ellipse(Size::splat(2.0 * radius), Some(Color::GREEN.into()), None), - Span::detached(), - ), - ); - } - - /// Add a green marker line at a position for debugging. - pub fn mark_line(&mut self, y: Abs) { - self.push( - Point::with_y(y), - FrameItem::Shape( - Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { - paint: Color::GREEN.into(), - thickness: Abs::pt(1.0), - ..Stroke::default() - }), - Span::detached(), - ), - ); - } -} - -impl Debug for Frame { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Frame ")?; - f.debug_list() - .entries(self.items.iter().map(|(_, item)| item)) - .finish() - } -} - -/// The building block frames are composed of. -#[derive(Clone, Hash)] -pub enum FrameItem { - /// A subframe with optional transformation and clipping. - Group(GroupItem), - /// A run of shaped text. - Text(TextItem), - /// A geometric shape with optional fill and stroke. - Shape(Shape, Span), - /// An image and its size. - Image(Image, Size, Span), - /// Meta information and the region it applies to. - Meta(Meta, Size), -} - -impl Debug for FrameItem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Group(group) => group.fmt(f), - Self::Text(text) => write!(f, "{text:?}"), - Self::Shape(shape, _) => write!(f, "{shape:?}"), - Self::Image(image, _, _) => write!(f, "{image:?}"), - Self::Meta(meta, _) => write!(f, "{meta:?}"), - } - } -} - -/// A subframe with optional transformation and clipping. -#[derive(Clone, Hash)] -pub struct GroupItem { - /// The group's frame. - pub frame: Frame, - /// A transformation to apply to the group. - pub transform: Transform, - /// Whether the frame should be a clipping boundary. - pub clips: bool, -} - -impl GroupItem { - /// Create a new group with default settings. - pub fn new(frame: Frame) -> Self { - Self { - frame, - transform: Transform::identity(), - clips: false, - } - } -} - -impl Debug for GroupItem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Group ")?; - self.frame.fmt(f) - } -} - -/// A run of shaped text. -#[derive(Clone, Eq, PartialEq, Hash)] -pub struct TextItem { - /// The font the glyphs are contained in. - pub font: Font, - /// The font size. - pub size: Abs, - /// Glyph color. - pub fill: Paint, - /// The natural language of the text. - pub lang: Lang, - /// The item's plain text. - pub text: EcoString, - /// The glyphs. - pub glyphs: Vec, -} - -impl TextItem { - /// The width of the text run. - pub fn width(&self) -> Abs { - self.glyphs.iter().map(|g| g.x_advance).sum::().at(self.size) - } -} - -impl Debug for TextItem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Text(")?; - self.text.fmt(f)?; - f.write_str(")") - } -} - -/// A glyph in a run of shaped text. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Glyph { - /// The glyph's index in the font. - pub id: u16, - /// The advance width of the glyph. - pub x_advance: Em, - /// The horizontal offset of the glyph. - pub x_offset: Em, - /// The range of the glyph in its item's text. - pub range: Range, - /// The source code location of the text. - pub span: (Span, u16), -} - -impl Glyph { - /// The range of the glyph in its item's text. - pub fn range(&self) -> Range { - usize::from(self.range.start)..usize::from(self.range.end) - } -} - -/// An identifier for a natural language. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Lang([u8; 3], u8); - -impl Lang { - pub const ALBANIAN: Self = Self(*b"sq ", 2); - pub const ARABIC: Self = Self(*b"ar ", 2); - pub const BOKMÅL: Self = Self(*b"nb ", 2); - pub const CHINESE: Self = Self(*b"zh ", 2); - pub const CZECH: Self = Self(*b"cs ", 2); - pub const DANISH: Self = Self(*b"da ", 2); - pub const DUTCH: Self = Self(*b"nl ", 2); - pub const ENGLISH: Self = Self(*b"en ", 2); - pub const FILIPINO: Self = Self(*b"tl ", 2); - pub const FRENCH: Self = Self(*b"fr ", 2); - pub const GERMAN: Self = Self(*b"de ", 2); - pub const ITALIAN: Self = Self(*b"it ", 2); - pub const JAPANESE: Self = Self(*b"ja ", 2); - pub const NYNORSK: Self = Self(*b"nn ", 2); - pub const POLISH: Self = Self(*b"pl ", 2); - pub const PORTUGUESE: Self = Self(*b"pt ", 2); - pub const RUSSIAN: Self = Self(*b"ru ", 2); - pub const SLOVENIAN: Self = Self(*b"sl ", 2); - pub const SPANISH: Self = Self(*b"es ", 2); - pub const SWEDISH: Self = Self(*b"sv ", 2); - pub const TURKISH: Self = Self(*b"tr ", 2); - pub const UKRAINIAN: Self = Self(*b"ua ", 2); - pub const VIETNAMESE: Self = Self(*b"vi ", 2); - - /// Return the language code as an all lowercase string slice. - pub fn as_str(&self) -> &str { - std::str::from_utf8(&self.0[..usize::from(self.1)]).unwrap_or_default() - } - - /// The default direction for the language. - pub fn dir(self) -> Dir { - match self.as_str() { - "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur" - | "yi" => Dir::RTL, - _ => Dir::LTR, - } - } -} - -impl FromStr for Lang { - type Err = &'static str; - - /// Construct a language from a two- or three-byte ISO 639-1/2/3 code. - fn from_str(iso: &str) -> Result { - let len = iso.len(); - if matches!(len, 2..=3) && iso.is_ascii() { - let mut bytes = [b' '; 3]; - bytes[..len].copy_from_slice(iso.as_bytes()); - bytes.make_ascii_lowercase(); - Ok(Self(bytes, len as u8)) - } else { - Err("expected two or three letter language code (ISO 639-1/2/3)") - } - } -} - -cast! { - Lang, - self => self.as_str().into_value(), - string: EcoString => Self::from_str(&string)?, -} - -/// An identifier for a region somewhere in the world. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Region([u8; 2]); - -impl Region { - /// Return the region code as an all uppercase string slice. - pub fn as_str(&self) -> &str { - std::str::from_utf8(&self.0).unwrap_or_default() - } -} - -impl PartialEq<&str> for Region { - fn eq(&self, other: &&str) -> bool { - self.as_str() == *other - } -} - -impl FromStr for Region { - type Err = &'static str; - - /// Construct a region from its two-byte ISO 3166-1 alpha-2 code. - fn from_str(iso: &str) -> Result { - if iso.len() == 2 && iso.is_ascii() { - let mut bytes: [u8; 2] = iso.as_bytes().try_into().unwrap(); - bytes.make_ascii_uppercase(); - Ok(Self(bytes)) - } else { - Err("expected two letter region code (ISO 3166-1 alpha-2)") - } - } -} - -cast! { - Region, - self => self.as_str().into_value(), - string: EcoString => Self::from_str(&string)?, -} - -/// Meta information that isn't visible or renderable. -#[derive(Clone, PartialEq, Hash)] -pub enum Meta { - /// An internal or external link to a destination. - Link(Destination), - /// An identifiable element that produces something within the area this - /// metadata is attached to. - Elem(Content), - /// The numbering of the current page. - PageNumbering(Value), - /// Indicates that content should be hidden. This variant doesn't appear - /// in the final frames as it is removed alongside the content that should - /// be hidden. - Hide, -} - -cast! { - type Meta: "meta", -} - -impl Debug for Meta { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Link(dest) => write!(f, "Link({dest:?})"), - Self::Elem(content) => write!(f, "Elem({:?})", content.func()), - Self::PageNumbering(value) => write!(f, "PageNumbering({value:?})"), - Self::Hide => f.pad("Hide"), - } - } -} - -/// A link destination. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum Destination { - /// A link to a URL. - Url(EcoString), - /// A link to a point on a page. - Position(Position), - /// An unresolved link to a location in the document. - Location(Location), -} - -cast! { - Destination, - self => match self { - Self::Url(v) => v.into_value(), - Self::Position(v) => v.into_value(), - Self::Location(v) => v.into_value(), - }, - v: EcoString => Self::Url(v), - v: Position => Self::Position(v), - v: Location => Self::Location(v), -} - -/// A physical position in a document. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Position { - /// The page, starting at 1. - pub page: NonZeroUsize, - /// The exact coordinates on the page (from the top left, as usual). - pub point: Point, -} - -cast! { - Position, - self => Value::Dict(self.into()), - mut dict: Dict => { - let page = dict.take("page")?.cast()?; - let x: Length = dict.take("x")?.cast()?; - let y: Length = dict.take("y")?.cast()?; - dict.finish(&["page", "x", "y"])?; - Self { page, point: Point::new(x.abs, y.abs) } - }, -} - -impl From for Dict { - fn from(pos: Position) -> Self { - dict! { - "page" => pos.page, - "x" => pos.point.x, - "y" => pos.point.y, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::util::option_eq; - - #[test] - fn test_region_option_eq() { - let region = Some(Region([b'U', b'S'])); - assert!(option_eq(region, "US")); - assert!(!option_eq(region, "AB")); - } - - #[test] - fn test_document_is_send() { - fn ensure_send() {} - ensure_send::(); - } -} diff --git a/src/eval/args.rs b/src/eval/args.rs deleted file mode 100644 index da29eeaf..00000000 --- a/src/eval/args.rs +++ /dev/null @@ -1,216 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use ecow::{eco_format, EcoVec}; - -use super::{Array, Dict, FromValue, IntoValue, Str, Value}; -use crate::diag::{bail, At, SourceResult}; -use crate::syntax::{Span, Spanned}; -use crate::util::pretty_array_like; - -/// Evaluated arguments to a function. -#[derive(Clone, PartialEq, Hash)] -pub struct Args { - /// The span of the whole argument list. - pub span: Span, - /// The positional and named arguments. - pub items: EcoVec, -} - -/// An argument to a function call: `12` or `draw: false`. -#[derive(Clone, PartialEq, Hash)] -pub struct Arg { - /// The span of the whole argument. - pub span: Span, - /// The name of the argument (`None` for positional arguments). - pub name: Option, - /// The value of the argument. - pub value: Spanned, -} - -impl Args { - /// Create positional arguments from a span and values. - pub fn new(span: Span, values: impl IntoIterator) -> Self { - let items = values - .into_iter() - .map(|value| Arg { - span, - name: None, - value: Spanned::new(value.into_value(), span), - }) - .collect(); - Self { span, items } - } - - /// Push a positional argument. - pub fn push(&mut self, span: Span, value: Value) { - self.items.push(Arg { - span: self.span, - name: None, - value: Spanned::new(value, span), - }) - } - - /// Consume and cast the first positional argument if there is one. - pub fn eat(&mut self) -> SourceResult> - where - T: FromValue>, - { - for (i, slot) in self.items.iter().enumerate() { - if slot.name.is_none() { - let value = self.items.remove(i).value; - let span = value.span; - return T::from_value(value).at(span).map(Some); - } - } - Ok(None) - } - - /// Consume n positional arguments if possible. - pub fn consume(&mut self, n: usize) -> SourceResult> { - let mut list = vec![]; - - let mut i = 0; - while i < self.items.len() && list.len() < n { - if self.items[i].name.is_none() { - list.push(self.items.remove(i)); - } else { - i += 1; - } - } - - if list.len() < n { - bail!(self.span, "not enough arguments"); - } - - Ok(list) - } - - /// Consume and cast the first positional argument. - /// - /// Returns a `missing argument: {what}` error if no positional argument is - /// left. - pub fn expect(&mut self, what: &str) -> SourceResult - where - T: FromValue>, - { - match self.eat()? { - Some(v) => Ok(v), - None => bail!(self.span, "missing argument: {what}"), - } - } - - /// Find and consume the first castable positional argument. - pub fn find(&mut self) -> SourceResult> - where - T: FromValue>, - { - for (i, slot) in self.items.iter().enumerate() { - if slot.name.is_none() && T::castable(&slot.value.v) { - let value = self.items.remove(i).value; - let span = value.span; - return T::from_value(value).at(span).map(Some); - } - } - Ok(None) - } - - /// Find and consume all castable positional arguments. - pub fn all(&mut self) -> SourceResult> - where - T: FromValue>, - { - let mut list = vec![]; - while let Some(value) = self.find()? { - list.push(value); - } - Ok(list) - } - - /// Cast and remove the value for the given named argument, returning an - /// error if the conversion fails. - pub fn named(&mut self, name: &str) -> SourceResult> - where - T: FromValue>, - { - // We don't quit once we have a match because when multiple matches - // exist, we want to remove all of them and use the last one. - let mut i = 0; - let mut found = None; - while i < self.items.len() { - if self.items[i].name.as_deref() == Some(name) { - let value = self.items.remove(i).value; - let span = value.span; - found = Some(T::from_value(value).at(span)?); - } else { - i += 1; - } - } - Ok(found) - } - - /// Same as named, but with fallback to find. - pub fn named_or_find(&mut self, name: &str) -> SourceResult> - where - T: FromValue>, - { - match self.named(name)? { - Some(value) => Ok(Some(value)), - None => self.find(), - } - } - - /// Take out all arguments into a new instance. - pub fn take(&mut self) -> Self { - Self { - span: self.span, - items: std::mem::take(&mut self.items), - } - } - - /// Return an "unexpected argument" error if there is any remaining - /// argument. - pub fn finish(self) -> SourceResult<()> { - if let Some(arg) = self.items.first() { - match &arg.name { - Some(name) => bail!(arg.span, "unexpected argument: {name}"), - _ => bail!(arg.span, "unexpected argument"), - } - } - Ok(()) - } - - /// Extract the positional arguments as an array. - pub fn to_pos(&self) -> Array { - self.items - .iter() - .filter(|item| item.name.is_none()) - .map(|item| item.value.v.clone()) - .collect() - } - - /// Extract the named arguments as a dictionary. - pub fn to_named(&self) -> Dict { - self.items - .iter() - .filter_map(|item| item.name.clone().map(|name| (name, item.value.v.clone()))) - .collect() - } -} - -impl Debug for Args { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let pieces: Vec<_> = - self.items.iter().map(|arg| eco_format!("{arg:?}")).collect(); - f.write_str(&pretty_array_like(&pieces, false)) - } -} - -impl Debug for Arg { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if let Some(name) = &self.name { - f.write_str(name)?; - f.write_str(": ")?; - } - Debug::fmt(&self.value.v, f) - } -} diff --git a/src/eval/array.rs b/src/eval/array.rs deleted file mode 100644 index a7a1387b..00000000 --- a/src/eval/array.rs +++ /dev/null @@ -1,508 +0,0 @@ -use std::cmp::Ordering; -use std::fmt::{self, Debug, Formatter}; -use std::ops::{Add, AddAssign}; - -use ecow::{eco_format, EcoString, EcoVec}; - -use super::{ops, Args, CastInfo, FromValue, Func, IntoValue, Reflect, Value, Vm}; -use crate::diag::{At, SourceResult, StrResult}; -use crate::syntax::Span; -use crate::util::pretty_array_like; - -/// Create a new [`Array`] from values. -#[macro_export] -#[doc(hidden)] -macro_rules! __array { - ($value:expr; $count:expr) => { - $crate::eval::Array::from($crate::eval::eco_vec![ - $crate::eval::IntoValue::into_value($value); - $count - ]) - }; - - ($($value:expr),* $(,)?) => { - $crate::eval::Array::from($crate::eval::eco_vec![$( - $crate::eval::IntoValue::into_value($value) - ),*]) - }; -} - -#[doc(inline)] -pub use crate::__array as array; -use crate::eval::ops::{add, mul}; -#[doc(hidden)] -pub use ecow::eco_vec; - -/// A reference counted array with value semantics. -#[derive(Default, Clone, PartialEq, Hash)] -pub struct Array(EcoVec); - -impl Array { - /// Create a new, empty array. - pub fn new() -> Self { - Self::default() - } - - /// Return `true` if the length is 0. - pub fn is_empty(&self) -> bool { - self.0.len() == 0 - } - - /// The length of the array. - pub fn len(&self) -> usize { - self.0.len() - } - - /// The first value in the array. - pub fn first(&self) -> StrResult<&Value> { - self.0.first().ok_or_else(array_is_empty) - } - - /// Mutably borrow the first value in the array. - pub fn first_mut(&mut self) -> StrResult<&mut Value> { - self.0.make_mut().first_mut().ok_or_else(array_is_empty) - } - - /// The last value in the array. - pub fn last(&self) -> StrResult<&Value> { - self.0.last().ok_or_else(array_is_empty) - } - - /// Mutably borrow the last value in the array. - pub fn last_mut(&mut self) -> StrResult<&mut Value> { - self.0.make_mut().last_mut().ok_or_else(array_is_empty) - } - - /// 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)) - .or(default) - .ok_or_else(|| out_of_bounds_no_default(index, self.len())) - } - - /// 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) - .and_then(move |i| self.0.make_mut().get_mut(i)) - .ok_or_else(|| out_of_bounds_no_default(index, len)) - } - - /// Push a value to the end of the array. - pub fn push(&mut self, value: Value) { - self.0.push(value); - } - - /// Remove the last value in the array. - pub fn pop(&mut self) -> StrResult { - self.0.pop().ok_or_else(array_is_empty) - } - - /// 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))?; - - 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))?; - - 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); - - Ok(self.0[start..end].into()) - } - - /// Whether the array contains a specific value. - pub fn contains(&self, value: &Value) -> bool { - self.0.contains(value) - } - - /// Return the first matching item. - pub fn find(&self, vm: &mut Vm, func: Func) -> SourceResult> { - for item in self.iter() { - let args = Args::new(func.span(), [item.clone()]); - if func.call_vm(vm, args)?.cast::().at(func.span())? { - return Ok(Some(item.clone())); - } - } - Ok(None) - } - - /// Return the index of the first matching item. - pub fn position(&self, vm: &mut Vm, func: Func) -> SourceResult> { - for (i, item) in self.iter().enumerate() { - let args = Args::new(func.span(), [item.clone()]); - if func.call_vm(vm, args)?.cast::().at(func.span())? { - return Ok(Some(i as i64)); - } - } - - Ok(None) - } - - /// Return a new array with only those items for which the function returns - /// true. - pub fn filter(&self, vm: &mut Vm, func: Func) -> SourceResult { - let mut kept = EcoVec::new(); - for item in self.iter() { - let args = Args::new(func.span(), [item.clone()]); - if func.call_vm(vm, args)?.cast::().at(func.span())? { - kept.push(item.clone()) - } - } - Ok(kept.into()) - } - - /// Transform each item in the array with a function. - pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult { - self.iter() - .map(|item| { - let args = Args::new(func.span(), [item.clone()]); - func.call_vm(vm, args) - }) - .collect() - } - - /// Fold all of the array's items into one with a function. - pub fn fold(&self, vm: &mut Vm, init: Value, func: Func) -> SourceResult { - let mut acc = init; - for item in self.iter() { - let args = Args::new(func.span(), [acc, item.clone()]); - acc = func.call_vm(vm, args)?; - } - Ok(acc) - } - - /// Calculates the sum of the array's items - pub fn sum(&self, default: Option, span: Span) -> SourceResult { - let mut acc = self - .first() - .map(|x| x.clone()) - .or_else(|_| { - default.ok_or_else(|| { - eco_format!("cannot calculate sum of empty array with no default") - }) - }) - .at(span)?; - for i in self.iter().skip(1) { - acc = add(acc, i.clone()).at(span)?; - } - Ok(acc) - } - - /// Calculates the product of the array's items - pub fn product(&self, default: Option, span: Span) -> SourceResult { - let mut acc = self - .first() - .map(|x| x.clone()) - .or_else(|_| { - default.ok_or_else(|| { - eco_format!("cannot calculate product of empty array with no default") - }) - }) - .at(span)?; - for i in self.iter().skip(1) { - acc = mul(acc, i.clone()).at(span)?; - } - Ok(acc) - } - - /// Whether any item matches. - pub fn any(&self, vm: &mut Vm, func: Func) -> SourceResult { - for item in self.iter() { - let args = Args::new(func.span(), [item.clone()]); - if func.call_vm(vm, args)?.cast::().at(func.span())? { - return Ok(true); - } - } - - Ok(false) - } - - /// Whether all items match. - pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult { - for item in self.iter() { - let args = Args::new(func.span(), [item.clone()]); - if !func.call_vm(vm, args)?.cast::().at(func.span())? { - return Ok(false); - } - } - - Ok(true) - } - - /// Return a new array with all items from this and nested arrays. - pub fn flatten(&self) -> Self { - let mut flat = EcoVec::with_capacity(self.0.len()); - for item in self.iter() { - if let Value::Array(nested) = item { - flat.extend(nested.flatten().into_iter()); - } else { - flat.push(item.clone()); - } - } - flat.into() - } - - /// Returns a new array with reversed order. - pub fn rev(&self) -> Self { - self.0.iter().cloned().rev().collect() - } - - /// Split all values in the array. - pub fn split(&self, at: Value) -> Array { - self.as_slice() - .split(|value| *value == at) - .map(|subslice| Value::Array(subslice.iter().cloned().collect())) - .collect() - } - - /// Join all values in the array, optionally with separator and last - /// separator (between the final two items). - pub fn join(&self, sep: Option, mut last: Option) -> StrResult { - let len = self.0.len(); - let sep = sep.unwrap_or(Value::None); - - let mut result = Value::None; - for (i, value) in self.iter().cloned().enumerate() { - if i > 0 { - if i + 1 == len && last.is_some() { - result = ops::join(result, last.take().unwrap())?; - } else { - result = ops::join(result, sep.clone())?; - } - } - - result = ops::join(result, value)?; - } - - Ok(result) - } - - /// Zips the array with another array. If the two arrays are of unequal length, it will only - /// zip up until the last element of the smaller array and the remaining elements will be - /// ignored. The return value is an array where each element is yet another array of size 2. - pub fn zip(&self, other: Array) -> Array { - self.iter() - .zip(other) - .map(|(first, second)| array![first.clone(), second].into_value()) - .collect() - } - - /// Return a sorted version of this array, optionally by a given key function. - /// - /// Returns an error if two values could not be compared or if the key function (if given) - /// yields an error. - pub fn sorted( - &self, - vm: &mut Vm, - span: Span, - key: Option, - ) -> SourceResult { - let mut result = Ok(()); - let mut vec = self.0.clone(); - let mut key_of = |x: Value| match &key { - // NOTE: We are relying on `comemo`'s memoization of function - // evaluation to not excessively reevaluate the `key`. - Some(f) => f.call_vm(vm, Args::new(f.span(), [x])), - None => Ok(x), - }; - vec.make_mut().sort_by(|a, b| { - // Until we get `try` blocks :) - match (key_of(a.clone()), key_of(b.clone())) { - (Ok(a), Ok(b)) => { - typst::eval::ops::compare(&a, &b).unwrap_or_else(|err| { - if result.is_ok() { - result = Err(err).at(span); - } - Ordering::Equal - }) - } - (Err(e), _) | (_, Err(e)) => { - if result.is_ok() { - result = Err(e); - } - Ordering::Equal - } - } - }); - result.map(|_| vec.into()) - } - - /// Repeat this array `n` times. - pub fn repeat(&self, n: i64) -> StrResult { - let count = usize::try_from(n) - .ok() - .and_then(|n| self.0.len().checked_mul(n)) - .ok_or_else(|| format!("cannot repeat this array {} times", n))?; - - 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) -> Self { - self.iter() - .enumerate() - .map(|(i, value)| array![i, value.clone()].into_value()) - .collect() - } -} - -impl Debug for Array { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let pieces: Vec<_> = self.iter().map(|value| eco_format!("{value:?}")).collect(); - f.write_str(&pretty_array_like(&pieces, self.len() == 1)) - } -} - -impl Add for Array { - type Output = Self; - - fn add(mut self, rhs: Array) -> Self::Output { - self += rhs; - self - } -} - -impl AddAssign for Array { - fn add_assign(&mut self, rhs: Array) { - self.0.extend(rhs.0); - } -} - -impl Extend for Array { - fn extend>(&mut self, iter: T) { - self.0.extend(iter); - } -} - -impl FromIterator for Array { - fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().collect()) - } -} - -impl IntoIterator for Array { - type Item = Value; - type IntoIter = ecow::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a> IntoIterator for &'a Array { - type Item = &'a Value; - type IntoIter = std::slice::Iter<'a, Value>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl From> for Array { - fn from(v: EcoVec) -> Self { - Array(v) - } -} - -impl From<&[Value]> for Array { - fn from(v: &[Value]) -> Self { - Array(v.into()) - } -} - -impl Reflect for Vec { - fn describe() -> CastInfo { - Array::describe() - } - - fn castable(value: &Value) -> bool { - Array::castable(value) - } -} - -impl IntoValue for Vec { - fn into_value(self) -> Value { - Value::Array(self.into_iter().map(IntoValue::into_value).collect()) - } -} - -impl FromValue for Vec { - fn from_value(value: Value) -> StrResult { - value.cast::()?.into_iter().map(Value::cast).collect() - } -} - -/// The error message when the array is empty. -#[cold] -fn array_is_empty() -> EcoString { - "array is empty".into() -} - -/// The out of bounds access error message. -#[cold] -fn out_of_bounds(index: i64, len: usize) -> EcoString { - eco_format!("array 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!( - "array index out of bounds (index: {index}, len: {len}) \ - and no default value was specified", - ) -} diff --git a/src/eval/auto.rs b/src/eval/auto.rs deleted file mode 100644 index e73b3f33..00000000 --- a/src/eval/auto.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use super::{CastInfo, FromValue, IntoValue, Reflect, Value}; -use crate::diag::StrResult; - -/// A value that indicates a smart default. -#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct AutoValue; - -impl IntoValue for AutoValue { - fn into_value(self) -> Value { - Value::Auto - } -} - -impl FromValue for AutoValue { - fn from_value(value: Value) -> StrResult { - match value { - Value::Auto => Ok(Self), - _ => Err(Self::error(&value)), - } - } -} - -impl Reflect for AutoValue { - fn describe() -> CastInfo { - CastInfo::Type("auto") - } - - fn castable(value: &Value) -> bool { - matches!(value, Value::Auto) - } -} - -impl Debug for AutoValue { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("auto") - } -} diff --git a/src/eval/cast.rs b/src/eval/cast.rs deleted file mode 100644 index 917972ed..00000000 --- a/src/eval/cast.rs +++ /dev/null @@ -1,316 +0,0 @@ -pub use typst_macros::{cast, Cast}; - -use std::fmt::Write; -use std::ops::Add; - -use ecow::EcoString; - -use super::Value; -use crate::diag::{At, SourceResult, StrResult}; -use crate::syntax::{Span, Spanned}; -use crate::util::separated_list; - -/// Determine details of a type. -/// -/// Type casting works as follows: -/// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` -/// (for documentation and autocomplete). -/// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` -/// (infallible) -/// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` -/// (fallible). -/// -/// We can't use `TryFrom` due to conflicting impls. We could use -/// `From for Value`, but that inverses the impl and leads to tons of -/// `.into()` all over the place that become hard to decipher. -pub trait Reflect { - /// Describe the acceptable values for this type. - fn describe() -> CastInfo; - - /// Whether the given value can be converted to `T`. - /// - /// This exists for performance. The check could also be done through the - /// [`CastInfo`], but it would be much more expensive (heap allocation + - /// dynamic checks instead of optimized machine code for each type). - fn castable(value: &Value) -> bool; - - /// Produce an error message for an inacceptable value. - /// - /// ``` - /// # use typst::eval::{Int, Reflect, Value}; - /// assert_eq!( - /// ::error(Value::None), - /// "expected integer, found none", - /// ); - /// ``` - fn error(found: &Value) -> EcoString { - Self::describe().error(found) - } -} - -impl Reflect for Value { - fn describe() -> CastInfo { - CastInfo::Any - } - - fn castable(_: &Value) -> bool { - true - } -} - -impl Reflect for Spanned { - fn describe() -> CastInfo { - T::describe() - } - - fn castable(value: &Value) -> bool { - T::castable(value) - } -} - -impl Reflect for StrResult { - fn describe() -> CastInfo { - T::describe() - } - - fn castable(value: &Value) -> bool { - T::castable(value) - } -} - -impl Reflect for SourceResult { - fn describe() -> CastInfo { - T::describe() - } - - fn castable(value: &Value) -> bool { - T::castable(value) - } -} - -impl Reflect for &T { - fn describe() -> CastInfo { - T::describe() - } - - fn castable(value: &Value) -> bool { - T::castable(value) - } -} - -impl Reflect for &mut T { - fn describe() -> CastInfo { - T::describe() - } - - fn castable(value: &Value) -> bool { - T::castable(value) - } -} - -/// Cast a Rust type into a Typst [`Value`]. -/// -/// See also: [`Reflect`]. -pub trait IntoValue { - /// Cast this type into a value. - fn into_value(self) -> Value; -} - -impl IntoValue for Value { - fn into_value(self) -> Value { - self - } -} - -impl IntoValue for Spanned { - fn into_value(self) -> Value { - self.v.into_value() - } -} - -/// Cast a Rust type or result into a [`SourceResult`]. -/// -/// Converts `T`, [`StrResult`], or [`SourceResult`] into -/// [`SourceResult`] by `Ok`-wrapping or adding span information. -pub trait IntoResult { - /// Cast this type into a value. - fn into_result(self, span: Span) -> SourceResult; -} - -impl IntoResult for T { - fn into_result(self, _: Span) -> SourceResult { - Ok(self.into_value()) - } -} - -impl IntoResult for StrResult { - fn into_result(self, span: Span) -> SourceResult { - self.map(IntoValue::into_value).at(span) - } -} - -impl IntoResult for SourceResult { - fn into_result(self, _: Span) -> SourceResult { - self.map(IntoValue::into_value) - } -} - -/// Try to cast a Typst [`Value`] into a Rust type. -/// -/// See also: [`Reflect`]. -pub trait FromValue: Sized + Reflect { - /// Try to cast the value into an instance of `Self`. - fn from_value(value: V) -> StrResult; -} - -impl FromValue for Value { - fn from_value(value: Value) -> StrResult { - Ok(value) - } -} - -impl FromValue> for T { - fn from_value(value: Spanned) -> StrResult { - T::from_value(value.v) - } -} - -impl FromValue> for Spanned { - fn from_value(value: Spanned) -> StrResult { - let span = value.span; - T::from_value(value.v).map(|t| Spanned::new(t, span)) - } -} - -/// Describes a possible value for a cast. -#[derive(Debug, Clone, Hash, PartialEq, PartialOrd)] -pub enum CastInfo { - /// Any value is okay. - Any, - /// A specific value, plus short documentation for that value. - Value(Value, &'static str), - /// Any value of a type. - Type(&'static str), - /// Multiple alternatives. - Union(Vec), -} - -impl CastInfo { - /// Produce an error message describing what was expected and what was - /// found. - pub fn error(&self, found: &Value) -> EcoString { - fn accumulate( - info: &CastInfo, - found: &Value, - parts: &mut Vec, - matching_type: &mut bool, - ) { - match info { - CastInfo::Any => parts.push("anything".into()), - CastInfo::Value(value, _) => { - parts.push(value.repr().into()); - if value.type_name() == found.type_name() { - *matching_type = true; - } - } - CastInfo::Type(ty) => parts.push((*ty).into()), - CastInfo::Union(options) => { - for option in options { - accumulate(option, found, parts, matching_type); - } - } - } - } - - let mut matching_type = false; - let mut parts = vec![]; - accumulate(self, found, &mut parts, &mut matching_type); - - let mut msg = String::from("expected "); - if parts.is_empty() { - msg.push_str(" nothing"); - } - - msg.push_str(&separated_list(&parts, "or")); - - if !matching_type { - msg.push_str(", found "); - msg.push_str(found.type_name()); - } - if_chain::if_chain! { - if let Value::Int(i) = found; - if parts.iter().any(|p| p == "length"); - if !matching_type; - then { - write!(msg, ": a length needs a unit - did you mean {i}pt?").unwrap(); - } - }; - - msg.into() - } -} - -impl Add for CastInfo { - type Output = Self; - - fn add(self, rhs: Self) -> Self { - Self::Union(match (self, rhs) { - (Self::Union(mut lhs), Self::Union(rhs)) => { - for cast in rhs { - if !lhs.contains(&cast) { - lhs.push(cast); - } - } - lhs - } - (Self::Union(mut lhs), rhs) => { - if !lhs.contains(&rhs) { - lhs.push(rhs); - } - lhs - } - (lhs, Self::Union(mut rhs)) => { - if !rhs.contains(&lhs) { - rhs.insert(0, lhs); - } - rhs - } - (lhs, rhs) => vec![lhs, rhs], - }) - } -} - -/// A container for a variadic argument. -pub trait Variadics { - /// The contained type. - type Inner; -} - -impl Variadics for Vec { - type Inner = T; -} - -/// An uninhabitable type. -pub enum Never {} - -impl Reflect for Never { - fn describe() -> CastInfo { - CastInfo::Union(vec![]) - } - - fn castable(_: &Value) -> bool { - false - } -} - -impl IntoValue for Never { - fn into_value(self) -> Value { - match self {} - } -} - -impl FromValue for Never { - fn from_value(value: Value) -> StrResult { - Err(Self::error(&value)) - } -} diff --git a/src/eval/datetime.rs b/src/eval/datetime.rs deleted file mode 100644 index f3c4a5a1..00000000 --- a/src/eval/datetime.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::fmt; -use std::fmt::{Debug, Formatter}; -use std::hash::Hash; - -use ecow::{eco_format, EcoString, EcoVec}; -use time::error::{Format, InvalidFormatDescription}; -use time::{format_description, PrimitiveDateTime}; - -use crate::eval::cast; -use crate::util::pretty_array_like; - -/// A datetime object that represents either a date, a time or a combination of -/// both. -#[derive(Clone, Copy, PartialEq, Hash)] -pub enum Datetime { - /// Representation as a date. - Date(time::Date), - /// Representation as a time. - Time(time::Time), - /// Representation as a combination of date and time. - Datetime(time::PrimitiveDateTime), -} - -impl Datetime { - /// Display the date and/or time in a certain format. - pub fn display(&self, pattern: Option) -> Result { - let pattern = pattern.as_ref().map(EcoString::as_str).unwrap_or(match self { - Datetime::Date(_) => "[year]-[month]-[day]", - Datetime::Time(_) => "[hour]:[minute]:[second]", - Datetime::Datetime(_) => "[year]-[month]-[day] [hour]:[minute]:[second]", - }); - - let format = format_description::parse(pattern) - .map_err(format_time_invalid_format_description_error)?; - - let formatted_result = match self { - Datetime::Date(date) => date.format(&format), - Datetime::Time(time) => time.format(&format), - Datetime::Datetime(datetime) => datetime.format(&format), - } - .map(EcoString::from); - - formatted_result.map_err(format_time_format_error) - } - - /// Return the year of the datetime, if existing. - pub fn year(&self) -> Option { - match self { - Datetime::Date(date) => Some(date.year()), - Datetime::Time(_) => None, - Datetime::Datetime(datetime) => Some(datetime.year()), - } - } - - /// Return the month of the datetime, if existing. - pub fn month(&self) -> Option { - match self { - Datetime::Date(date) => Some(date.month().into()), - Datetime::Time(_) => None, - Datetime::Datetime(datetime) => Some(datetime.month().into()), - } - } - - /// Return the weekday of the datetime, if existing. - pub fn weekday(&self) -> Option { - match self { - Datetime::Date(date) => Some(date.weekday().number_from_monday()), - Datetime::Time(_) => None, - Datetime::Datetime(datetime) => Some(datetime.weekday().number_from_monday()), - } - } - - /// Return the day of the datetime, if existing. - pub fn day(&self) -> Option { - match self { - Datetime::Date(date) => Some(date.day()), - Datetime::Time(_) => None, - Datetime::Datetime(datetime) => Some(datetime.day()), - } - } - - /// Return the hour of the datetime, if existing. - pub fn hour(&self) -> Option { - match self { - Datetime::Date(_) => None, - Datetime::Time(time) => Some(time.hour()), - Datetime::Datetime(datetime) => Some(datetime.hour()), - } - } - - /// Return the minute of the datetime, if existing. - pub fn minute(&self) -> Option { - match self { - Datetime::Date(_) => None, - Datetime::Time(time) => Some(time.minute()), - Datetime::Datetime(datetime) => Some(datetime.minute()), - } - } - - /// Return the second of the datetime, if existing. - pub fn second(&self) -> Option { - match self { - Datetime::Date(_) => None, - Datetime::Time(time) => Some(time.second()), - Datetime::Datetime(datetime) => Some(datetime.second()), - } - } - - /// Create a datetime from year, month, and day. - pub fn from_ymd(year: i32, month: u8, day: u8) -> Option { - Some(Datetime::Date( - time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) - .ok()?, - )) - } - - /// Create a datetime from hour, minute, and second. - pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option { - Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?)) - } - - /// Create a datetime from day and time. - pub fn from_ymd_hms( - year: i32, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> Option { - let date = - time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day) - .ok()?; - let time = time::Time::from_hms(hour, minute, second).ok()?; - Some(Datetime::Datetime(PrimitiveDateTime::new(date, time))) - } -} - -impl Debug for Datetime { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let year = self.year().map(|y| eco_format!("year: {y}")); - let month = self.month().map(|m| eco_format!("month: {m}")); - let day = self.day().map(|d| eco_format!("day: {d}")); - let hour = self.hour().map(|h| eco_format!("hour: {h}")); - let minute = self.minute().map(|m| eco_format!("minute: {m}")); - let second = self.second().map(|s| eco_format!("second: {s}")); - let filtered = [year, month, day, hour, minute, second] - .into_iter() - .flatten() - .collect::>(); - - write!(f, "datetime{}", &pretty_array_like(&filtered, false)) - } -} - -cast! { - type Datetime: "datetime", -} - -/// Format the `Format` error of the time crate in an appropriate way. -fn format_time_format_error(error: Format) -> EcoString { - match error { - Format::InvalidComponent(name) => eco_format!("invalid component '{}'", name), - _ => "failed to format datetime in the requested format".into(), - } -} - -/// Format the `InvalidFormatDescription` error of the time crate in an -/// appropriate way. -fn format_time_invalid_format_description_error( - error: InvalidFormatDescription, -) -> EcoString { - match error { - InvalidFormatDescription::UnclosedOpeningBracket { index, .. } => { - eco_format!("missing closing bracket for bracket at index {}", index) - } - InvalidFormatDescription::InvalidComponentName { name, index, .. } => { - eco_format!("invalid component name '{}' at index {}", name, index) - } - InvalidFormatDescription::InvalidModifier { value, index, .. } => { - eco_format!("invalid modifier '{}' at index {}", value, index) - } - InvalidFormatDescription::Expected { what, index, .. } => { - eco_format!("expected {} at index {}", what, index) - } - InvalidFormatDescription::MissingComponentName { index, .. } => { - eco_format!("expected component name at index {}", index) - } - InvalidFormatDescription::MissingRequiredModifier { name, index, .. } => { - eco_format!( - "missing required modifier {} for component at index {}", - name, - index - ) - } - InvalidFormatDescription::NotSupported { context, what, index, .. } => { - eco_format!("{} is not supported in {} at index {}", what, context, index) - } - _ => "failed to parse datetime format".into(), - } -} diff --git a/src/eval/dict.rs b/src/eval/dict.rs deleted file mode 100644 index 3e6233ae..00000000 --- a/src/eval/dict.rs +++ /dev/null @@ -1,235 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::ops::{Add, AddAssign}; -use std::sync::Arc; - -use ecow::{eco_format, EcoString}; - -use super::{array, Array, Str, Value}; -use crate::diag::StrResult; -use crate::syntax::is_ident; -use crate::util::{pretty_array_like, separated_list, ArcExt}; - -/// Create a new [`Dict`] from key-value pairs. -#[macro_export] -#[doc(hidden)] -macro_rules! __dict { - ($($key:expr => $value:expr),* $(,)?) => {{ - #[allow(unused_mut)] - let mut map = $crate::eval::IndexMap::new(); - $(map.insert($key.into(), $crate::eval::IntoValue::into_value($value));)* - $crate::eval::Dict::from(map) - }}; -} - -#[doc(inline)] -pub use crate::__dict as dict; - -#[doc(inline)] -pub use indexmap::IndexMap; - -/// A reference-counted dictionary with value semantics. -#[derive(Default, Clone, PartialEq)] -pub struct Dict(Arc>); - -impl Dict { - /// Create a new, empty dictionary. - pub fn new() -> Self { - Self::default() - } - - /// Whether the dictionary is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// The number of pairs in the dictionary. - pub fn len(&self) -> usize { - self.0.len() - } - - /// 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)) - } - - /// Mutably borrow the value the given `key` maps to. - pub fn at_mut(&mut self, key: &str) -> StrResult<&mut Value> { - Arc::make_mut(&mut self.0) - .get_mut(key) - .ok_or_else(|| missing_key_no_default(key)) - } - - /// Remove the value if the dictionary contains the given key. - pub fn take(&mut self, key: &str) -> StrResult { - Arc::make_mut(&mut self.0) - .remove(key) - .ok_or_else(|| eco_format!("missing key: {:?}", Str::from(key))) - } - - /// Whether the dictionary contains a specific key. - pub fn contains(&self, key: &str) -> bool { - self.0.contains_key(key) - } - - /// Insert a mapping from the given `key` to the given `value`. - pub fn insert(&mut self, key: Str, value: Value) { - Arc::make_mut(&mut self.0).insert(key, value); - } - - /// Remove a mapping by `key` and return the value. - pub fn remove(&mut self, key: &str) -> StrResult { - match Arc::make_mut(&mut self.0).shift_remove(key) { - Some(value) => Ok(value), - None => Err(missing_key(key)), - } - } - - /// Clear the dictionary. - pub fn clear(&mut self) { - if Arc::strong_count(&self.0) == 1 { - Arc::make_mut(&mut self.0).clear(); - } else { - *self = Self::new(); - } - } - - /// Return the keys of the dictionary as an array. - pub fn keys(&self) -> Array { - self.0.keys().cloned().map(Value::Str).collect() - } - - /// Return the values of the dictionary as an array. - pub fn values(&self) -> Array { - self.0.values().cloned().collect() - } - - /// Return the values of the dictionary as an array of pairs (arrays of - /// length two). - pub fn pairs(&self) -> Array { - self.0 - .iter() - .map(|(k, v)| Value::Array(array![k.clone(), v.clone()])) - .collect() - } - - /// Iterate over pairs of references to the contained keys and values. - pub fn iter(&self) -> indexmap::map::Iter { - self.0.iter() - } - - /// Return an "unexpected key" error if there is any remaining pair. - pub fn finish(&self, expected: &[&str]) -> StrResult<()> { - if let Some((key, _)) = self.iter().next() { - let parts: Vec<_> = expected.iter().map(|s| eco_format!("\"{s}\"")).collect(); - let mut msg = format!("unexpected key {key:?}, valid keys are "); - msg.push_str(&separated_list(&parts, "and")); - return Err(msg.into()); - } - Ok(()) - } -} - -impl Debug for Dict { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if self.is_empty() { - return f.write_str("(:)"); - } - - let pieces: Vec<_> = self - .iter() - .map(|(key, value)| { - if is_ident(key) { - eco_format!("{key}: {value:?}") - } else { - eco_format!("{key:?}: {value:?}") - } - }) - .collect(); - - f.write_str(&pretty_array_like(&pieces, false)) - } -} - -impl Add for Dict { - type Output = Self; - - fn add(mut self, rhs: Dict) -> Self::Output { - self += rhs; - self - } -} - -impl AddAssign for Dict { - fn add_assign(&mut self, rhs: Dict) { - match Arc::try_unwrap(rhs.0) { - Ok(map) => self.extend(map), - Err(rc) => self.extend(rc.iter().map(|(k, v)| (k.clone(), v.clone()))), - } - } -} - -impl Hash for Dict { - fn hash(&self, state: &mut H) { - state.write_usize(self.0.len()); - for item in self { - item.hash(state); - } - } -} - -impl Extend<(Str, Value)> for Dict { - fn extend>(&mut self, iter: T) { - Arc::make_mut(&mut self.0).extend(iter); - } -} - -impl FromIterator<(Str, Value)> for Dict { - fn from_iter>(iter: T) -> Self { - Self(Arc::new(iter.into_iter().collect())) - } -} - -impl IntoIterator for Dict { - type Item = (Str, Value); - type IntoIter = indexmap::map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - Arc::take(self.0).into_iter() - } -} - -impl<'a> IntoIterator for &'a Dict { - type Item = (&'a Str, &'a Value); - type IntoIter = indexmap::map::Iter<'a, Str, Value>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl From> for Dict { - fn from(map: IndexMap) -> Self { - Self(Arc::new(map)) - } -} - -/// The missing key access error message. -#[cold] -fn missing_key(key: &str) -> EcoString { - eco_format!("dictionary does not contain key {:?}", Str::from(key)) -} - -/// The missing key access error message when no default was fiven. -#[cold] -fn missing_key_no_default(key: &str) -> EcoString { - eco_format!( - "dictionary does not contain key {:?} \ - and no default value was specified", - Str::from(key) - ) -} diff --git a/src/eval/func.rs b/src/eval/func.rs deleted file mode 100644 index 22f948ce..00000000 --- a/src/eval/func.rs +++ /dev/null @@ -1,643 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::sync::Arc; - -use comemo::{Prehashed, Tracked, TrackedMut}; -use ecow::eco_format; -use once_cell::sync::Lazy; - -use super::{ - cast, Args, CastInfo, Eval, FlowEvent, IntoValue, Route, Scope, Scopes, Tracer, - Value, Vm, -}; -use crate::diag::{bail, SourceResult, StrResult}; -use crate::file::FileId; -use crate::model::{DelayedErrors, ElemFunc, Introspector, Locator, Vt}; -use crate::syntax::ast::{self, AstNode, Expr, Ident}; -use crate::syntax::{Span, SyntaxNode}; -use crate::World; - -/// An evaluatable function. -#[derive(Clone, Hash)] -#[allow(clippy::derived_hash_with_manual_eq)] -pub struct Func { - /// The internal representation. - repr: Repr, - /// The span with which errors are reported when this function is called. - span: Span, -} - -/// The different kinds of function representations. -#[derive(Clone, PartialEq, Hash)] -enum Repr { - /// A native Rust function. - Native(&'static NativeFunc), - /// A function for an element. - Elem(ElemFunc), - /// A user-defined closure. - Closure(Arc>), - /// A nested function with pre-applied arguments. - With(Arc<(Func, Args)>), -} - -impl Func { - /// The name of the function. - pub fn name(&self) -> Option<&str> { - match &self.repr { - Repr::Native(native) => Some(native.info.name), - Repr::Elem(func) => Some(func.info().name), - Repr::Closure(closure) => closure.name.as_deref(), - Repr::With(arc) => arc.0.name(), - } - } - - /// Extract details the function. - pub fn info(&self) -> Option<&FuncInfo> { - match &self.repr { - Repr::Native(native) => Some(&native.info), - Repr::Elem(func) => Some(func.info()), - Repr::Closure(_) => None, - Repr::With(arc) => arc.0.info(), - } - } - - /// The function's span. - pub fn span(&self) -> Span { - self.span - } - - /// Attach a span to this function if it doesn't already have one. - pub fn spanned(mut self, span: Span) -> Self { - if self.span.is_detached() { - self.span = span; - } - self - } - - /// Call the function with the given arguments. - pub fn call_vm(&self, vm: &mut Vm, mut args: Args) -> SourceResult { - let _span = tracing::info_span!( - "call", - name = self.name().unwrap_or(""), - file = 0, - ); - - match &self.repr { - Repr::Native(native) => { - let value = (native.func)(vm, &mut args)?; - args.finish()?; - Ok(value) - } - Repr::Elem(func) => { - let value = func.construct(vm, &mut args)?; - args.finish()?; - Ok(Value::Content(value)) - } - Repr::Closure(closure) => { - // Determine the route inside the closure. - let fresh = Route::new(closure.location); - let route = - if vm.location.is_detached() { fresh.track() } else { vm.route }; - - Closure::call( - self, - vm.world(), - route, - vm.vt.introspector, - vm.vt.locator.track(), - TrackedMut::reborrow_mut(&mut vm.vt.delayed), - TrackedMut::reborrow_mut(&mut vm.vt.tracer), - vm.depth + 1, - args, - ) - } - Repr::With(arc) => { - args.items = arc.1.items.iter().cloned().chain(args.items).collect(); - arc.0.call_vm(vm, args) - } - } - } - - /// Call the function with a Vt. - #[tracing::instrument(skip_all)] - pub fn call_vt( - &self, - vt: &mut Vt, - args: impl IntoIterator, - ) -> SourceResult { - let route = Route::default(); - let scopes = Scopes::new(None); - let mut locator = Locator::chained(vt.locator.track()); - let vt = Vt { - world: vt.world, - introspector: vt.introspector, - locator: &mut locator, - delayed: TrackedMut::reborrow_mut(&mut vt.delayed), - tracer: TrackedMut::reborrow_mut(&mut vt.tracer), - }; - let mut vm = Vm::new(vt, route.track(), FileId::detached(), scopes); - let args = Args::new(self.span(), args); - self.call_vm(&mut vm, args) - } - - /// Apply the given arguments to the function. - pub fn with(self, args: Args) -> Self { - let span = self.span; - Self { repr: Repr::With(Arc::new((self, args))), span } - } - - /// Extract the element function, if it is one. - pub fn element(&self) -> Option { - match self.repr { - Repr::Elem(func) => Some(func), - _ => None, - } - } - - /// Get a field from this function's scope, if possible. - pub fn get(&self, field: &str) -> StrResult<&Value> { - match &self.repr { - Repr::Native(func) => func.info.scope.get(field).ok_or_else(|| { - eco_format!( - "function `{}` does not contain field `{}`", - func.info.name, - field - ) - }), - Repr::Elem(func) => func.info().scope.get(field).ok_or_else(|| { - eco_format!( - "function `{}` does not contain field `{}`", - func.name(), - field - ) - }), - Repr::Closure(_) => { - Err(eco_format!("cannot access fields on user-defined functions")) - } - Repr::With(arc) => arc.0.get(field), - } - } -} - -impl Debug for Func { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self.name() { - Some(name) => write!(f, "{name}"), - None => f.write_str("(..) => .."), - } - } -} - -impl PartialEq for Func { - fn eq(&self, other: &Self) -> bool { - self.repr == other.repr - } -} - -impl From for Func { - fn from(repr: Repr) -> Self { - Self { repr, span: Span::detached() } - } -} - -impl From for Func { - fn from(func: ElemFunc) -> Self { - Repr::Elem(func).into() - } -} - -/// A Typst function defined by a native Rust function. -pub struct NativeFunc { - /// The function's implementation. - pub func: fn(&mut Vm, &mut Args) -> SourceResult, - /// Details about the function. - pub info: Lazy, -} - -impl PartialEq for NativeFunc { - fn eq(&self, other: &Self) -> bool { - self.func as usize == other.func as usize - } -} - -impl Eq for NativeFunc {} - -impl Hash for NativeFunc { - fn hash(&self, state: &mut H) { - (self.func as usize).hash(state); - } -} - -impl From<&'static NativeFunc> for Func { - fn from(native: &'static NativeFunc) -> Self { - Repr::Native(native).into() - } -} - -cast! { - &'static NativeFunc, - self => Value::Func(self.into()), -} - -/// Details about a function. -#[derive(Debug, Clone)] -pub struct FuncInfo { - /// The function's name. - pub name: &'static str, - /// The display name of the function. - pub display: &'static str, - /// A string of search keywords. - pub keywords: Option<&'static str>, - /// Which category the function is part of. - pub category: &'static str, - /// Documentation for the function. - pub docs: &'static str, - /// Details about the function's parameters. - pub params: Vec, - /// Valid values for the return value. - pub returns: CastInfo, - /// The function's own scope of fields and sub-functions. - pub scope: Scope, -} - -impl FuncInfo { - /// Get the parameter info for a parameter with the given name - pub fn param(&self, name: &str) -> Option<&ParamInfo> { - self.params.iter().find(|param| param.name == name) - } -} - -/// Describes a named parameter. -#[derive(Debug, Clone)] -pub struct ParamInfo { - /// The parameter's name. - pub name: &'static str, - /// Documentation for the parameter. - pub docs: &'static str, - /// Valid values for the parameter. - pub cast: CastInfo, - /// Creates an instance of the parameter's default value. - pub default: Option Value>, - /// Is the parameter positional? - pub positional: bool, - /// Is the parameter named? - /// - /// Can be true even if `positional` is true if the parameter can be given - /// in both variants. - pub named: bool, - /// Can the parameter be given any number of times? - pub variadic: bool, - /// Is the parameter required? - pub required: bool, - /// Is the parameter settable with a set rule? - pub settable: bool, -} - -/// A user-defined closure. -#[derive(Hash)] -pub(super) struct Closure { - /// The source file where the closure was defined. - pub location: FileId, - /// The name of the closure. - pub name: Option, - /// Captured values from outer scopes. - pub captured: Scope, - /// The list of parameters. - pub params: Vec, - /// The expression the closure should evaluate to. - pub body: Expr, -} - -/// A closure parameter. -#[derive(Hash)] -pub enum Param { - /// A positional parameter: `x`. - Pos(ast::Pattern), - /// A named parameter with a default value: `draw: false`. - Named(Ident, Value), - /// An argument sink: `..args`. - Sink(Option), -} - -impl Closure { - /// Call the function in the context with the arguments. - #[comemo::memoize] - #[tracing::instrument(skip_all)] - #[allow(clippy::too_many_arguments)] - fn call( - this: &Func, - world: Tracked, - route: Tracked, - introspector: Tracked, - locator: Tracked, - delayed: TrackedMut, - tracer: TrackedMut, - depth: usize, - mut args: Args, - ) -> SourceResult { - let closure = match &this.repr { - Repr::Closure(closure) => closure, - _ => panic!("`this` must be a closure"), - }; - - // Don't leak the scopes from the call site. Instead, we use the scope - // of captured variables we collected earlier. - let mut scopes = Scopes::new(None); - scopes.top = closure.captured.clone(); - - // Prepare VT. - let mut locator = Locator::chained(locator); - let vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - - // Prepare VM. - let mut vm = Vm::new(vt, route, closure.location, scopes); - vm.depth = depth; - - // Provide the closure itself for recursive calls. - if let Some(name) = &closure.name { - vm.define(name.clone(), Value::Func(this.clone())); - } - - // Parse the arguments according to the parameter list. - let num_pos_params = - closure.params.iter().filter(|p| matches!(p, Param::Pos(_))).count(); - let num_pos_args = args.to_pos().len(); - let sink_size = num_pos_args.checked_sub(num_pos_params); - - let mut sink = None; - let mut sink_pos_values = None; - for p in &closure.params { - match p { - Param::Pos(pattern) => match pattern { - ast::Pattern::Normal(ast::Expr::Ident(ident)) => { - vm.define(ident.clone(), args.expect::(ident)?) - } - ast::Pattern::Normal(_) => unreachable!(), - _ => { - pattern.define( - &mut vm, - args.expect::("pattern parameter")?, - )?; - } - }, - Param::Sink(ident) => { - sink = ident.clone(); - if let Some(sink_size) = sink_size { - sink_pos_values = Some(args.consume(sink_size)?); - } - } - Param::Named(ident, default) => { - let value = - args.named::(ident)?.unwrap_or_else(|| default.clone()); - vm.define(ident.clone(), value); - } - } - } - - if let Some(sink) = sink { - let mut remaining_args = args.take(); - if let Some(sink_pos_values) = sink_pos_values { - remaining_args.items.extend(sink_pos_values); - } - vm.define(sink, remaining_args); - } - - // Ensure all arguments have been used. - args.finish()?; - - // Handle control flow. - let result = closure.body.eval(&mut vm); - match vm.flow { - Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit), - Some(FlowEvent::Return(_, None)) => {} - Some(flow) => bail!(flow.forbidden()), - None => {} - } - - result - } -} - -impl From for Func { - fn from(closure: Closure) -> Self { - Repr::Closure(Arc::new(Prehashed::new(closure))).into() - } -} - -cast! { - Closure, - self => Value::Func(self.into()), -} - -/// A visitor that determines which variables to capture for a closure. -pub(super) struct CapturesVisitor<'a> { - external: &'a Scopes<'a>, - internal: Scopes<'a>, - captures: Scope, -} - -impl<'a> CapturesVisitor<'a> { - /// Create a new visitor for the given external scopes. - pub fn new(external: &'a Scopes) -> Self { - Self { - external, - internal: Scopes::new(None), - captures: Scope::new(), - } - } - - /// Return the scope of captured variables. - pub fn finish(self) -> Scope { - self.captures - } - - /// Visit any node and collect all captured variables. - #[tracing::instrument(skip_all)] - pub fn visit(&mut self, node: &SyntaxNode) { - match node.cast() { - // Every identifier is a potential variable that we need to capture. - // Identifiers that shouldn't count as captures because they - // actually bind a new name are handled below (individually through - // the expressions that contain them). - Some(ast::Expr::Ident(ident)) => self.capture(ident), - Some(ast::Expr::MathIdent(ident)) => self.capture_in_math(ident), - - // Code and content blocks create a scope. - Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { - self.internal.enter(); - for child in node.children() { - self.visit(child); - } - self.internal.exit(); - } - - // A closure contains parameter bindings, which are bound before the - // body is evaluated. Care must be taken so that the default values - // of named parameters cannot access previous parameter bindings. - Some(ast::Expr::Closure(expr)) => { - for param in expr.params().children() { - if let ast::Param::Named(named) = param { - self.visit(named.expr().as_untyped()); - } - } - - self.internal.enter(); - if let Some(name) = expr.name() { - self.bind(name); - } - - for param in expr.params().children() { - match param { - ast::Param::Pos(pattern) => { - for ident in pattern.idents() { - self.bind(ident); - } - } - ast::Param::Named(named) => self.bind(named.name()), - ast::Param::Sink(spread) => { - self.bind(spread.name().unwrap_or_default()) - } - } - } - - self.visit(expr.body().as_untyped()); - self.internal.exit(); - } - - // A let expression contains a binding, but that binding is only - // active after the body is evaluated. - Some(ast::Expr::Let(expr)) => { - if let Some(init) = expr.init() { - self.visit(init.as_untyped()); - } - - for ident in expr.kind().idents() { - self.bind(ident); - } - } - - // A for loop contains one or two bindings in its pattern. These are - // active after the iterable is evaluated but before the body is - // evaluated. - Some(ast::Expr::For(expr)) => { - self.visit(expr.iter().as_untyped()); - self.internal.enter(); - - let pattern = expr.pattern(); - for ident in pattern.idents() { - self.bind(ident); - } - - self.visit(expr.body().as_untyped()); - self.internal.exit(); - } - - // An import contains items, but these are active only after the - // path is evaluated. - Some(ast::Expr::Import(expr)) => { - self.visit(expr.source().as_untyped()); - if let Some(ast::Imports::Items(items)) = expr.imports() { - for item in items { - self.bind(item); - } - } - } - - // Everything else is traversed from left to right. - _ => { - for child in node.children() { - self.visit(child); - } - } - } - } - - /// Bind a new internal variable. - fn bind(&mut self, ident: ast::Ident) { - self.internal.top.define(ident.take(), Value::None); - } - - /// Capture a variable if it isn't internal. - fn capture(&mut self, ident: ast::Ident) { - if self.internal.get(&ident).is_err() { - if let Ok(value) = self.external.get(&ident) { - self.captures.define_captured(ident.take(), value.clone()); - } - } - } - - /// Capture a variable in math mode if it isn't internal. - fn capture_in_math(&mut self, ident: ast::MathIdent) { - if self.internal.get(&ident).is_err() { - if let Ok(value) = self.external.get_in_math(&ident) { - self.captures.define_captured(ident.take(), value.clone()); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::syntax::parse; - - #[track_caller] - fn test(text: &str, result: &[&str]) { - let mut scopes = Scopes::new(None); - scopes.top.define("f", 0); - scopes.top.define("x", 0); - scopes.top.define("y", 0); - scopes.top.define("z", 0); - - let mut visitor = CapturesVisitor::new(&scopes); - let root = parse(text); - visitor.visit(&root); - - let captures = visitor.finish(); - let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect(); - names.sort(); - - assert_eq!(names, result); - } - - #[test] - fn test_captures() { - // Let binding and function definition. - test("#let x = x", &["x"]); - test("#let x; #(x + y)", &["y"]); - test("#let f(x, y) = x + y", &[]); - test("#let f(x, y) = f", &[]); - test("#let f = (x, y) => f", &["f"]); - - // Closure with different kinds of params. - test("#((x, y) => x + z)", &["z"]); - test("#((x: y, z) => x + z)", &["y"]); - test("#((..x) => x + y)", &["y"]); - test("#((x, y: x + z) => x + y)", &["x", "z"]); - test("#{x => x; x}", &["x"]); - - // Show rule. - test("#show y: x => x", &["y"]); - test("#show y: x => x + z", &["y", "z"]); - test("#show x: x => x", &["x"]); - - // For loop. - test("#for x in y { x + z }", &["y", "z"]); - test("#for (x, y) in y { x + y }", &["y"]); - test("#for x in y {} #x", &["x", "y"]); - - // Import. - test("#import z: x, y", &["z"]); - test("#import x + y: x, y, z", &["x", "y"]); - - // Blocks. - test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); - test("#[#let x = 1]#x", &["x"]); - } -} diff --git a/src/eval/int.rs b/src/eval/int.rs deleted file mode 100644 index 4e081617..00000000 --- a/src/eval/int.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize}; - -use super::{cast, Value}; - -macro_rules! signed_int { - ($($ty:ty)*) => { - $(cast! { - $ty, - self => Value::Int(self as i64), - v: i64 => v.try_into().map_err(|_| "number too large")?, - })* - } -} - -macro_rules! unsigned_int { - ($($ty:ty)*) => { - $(cast! { - $ty, - self => Value::Int(self as i64), - v: i64 => v.try_into().map_err(|_| { - if v < 0 { - "number must be at least zero" - } else { - "number too large" - } - })?, - })* - } -} - -macro_rules! signed_nonzero { - ($($ty:ty)*) => { - $(cast! { - $ty, - self => Value::Int(self.get() as i64), - v: i64 => v - .try_into() - .ok() - .and_then($ty::new) - .ok_or_else(|| if v == 0 { - "number must not be zero" - } else { - "number too large" - })?, - })* - } -} - -macro_rules! unsigned_nonzero { - ($($ty:ty)*) => { - $(cast! { - $ty, - self => Value::Int(self.get() as i64), - v: i64 => v - .try_into() - .ok() - .and_then($ty::new) - .ok_or_else(|| if v <= 0 { - "number must be positive" - } else { - "number too large" - })?, - })* - } -} - -signed_int! { - i8 i16 i32 isize -} - -unsigned_int! { - u8 u16 u32 u64 usize -} - -signed_nonzero! { - NonZeroI64 NonZeroIsize -} - -unsigned_nonzero! { - NonZeroU64 NonZeroUsize -} diff --git a/src/eval/library.rs b/src/eval/library.rs deleted file mode 100644 index 1b05de83..00000000 --- a/src/eval/library.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::num::NonZeroUsize; - -use comemo::Tracked; -use ecow::EcoString; -use std::sync::OnceLock; - -use super::{Args, Dynamic, Module, Value, Vm}; -use crate::diag::SourceResult; -use crate::doc::Document; -use crate::geom::{Abs, Dir}; -use crate::model::{Content, ElemFunc, Introspector, Label, StyleChain, Styles, Vt}; -use crate::syntax::Span; -use crate::util::hash128; - -/// Definition of Typst's standard library. -#[derive(Debug, Clone, Hash)] -pub struct Library { - /// The scope containing definitions that are available everywhere. - pub global: Module, - /// The scope containing definitions available in math mode. - pub math: Module, - /// The default properties for page size, font selection and so on. - pub styles: Styles, - /// Defines which standard library items fulfill which syntactical roles. - pub items: LangItems, -} - -/// Definition of library items the language is aware of. -#[derive(Clone)] -pub struct LangItems { - /// The root layout function. - pub layout: - fn(vt: &mut Vt, content: &Content, styles: StyleChain) -> SourceResult, - /// Access the em size. - pub em: fn(StyleChain) -> Abs, - /// Access the text direction. - pub dir: fn(StyleChain) -> Dir, - /// Whitespace. - pub space: fn() -> Content, - /// A forced line break: `\`. - pub linebreak: fn() -> Content, - /// Plain text without markup. - pub text: fn(text: EcoString) -> Content, - /// The text function. - pub text_func: ElemFunc, - /// Get the string if this is a text element. - pub text_str: fn(&Content) -> Option, - /// A smart quote: `'` or `"`. - pub smart_quote: fn(double: bool) -> Content, - /// A paragraph break. - pub parbreak: fn() -> Content, - /// Strong content: `*Strong*`. - pub strong: fn(body: Content) -> Content, - /// Emphasized content: `_Emphasized_`. - pub emph: fn(body: Content) -> Content, - /// Raw text with optional syntax highlighting: `` `...` ``. - pub raw: fn(text: EcoString, tag: Option, block: bool) -> Content, - /// The language names and tags supported by raw text. - pub raw_languages: fn() -> Vec<(&'static str, Vec<&'static str>)>, - /// A hyperlink: `https://typst.org`. - pub link: fn(url: EcoString) -> Content, - /// A reference: `@target`, `@target[..]`. - pub reference: fn(target: Label, supplement: Option) -> Content, - /// The keys contained in the bibliography and short descriptions of them. - #[allow(clippy::type_complexity)] - pub bibliography_keys: - fn(introspector: Tracked) -> Vec<(EcoString, Option)>, - /// A section heading: `= Introduction`. - pub heading: fn(level: NonZeroUsize, body: Content) -> Content, - /// The heading function. - pub heading_func: ElemFunc, - /// An item in a bullet list: `- ...`. - pub list_item: fn(body: Content) -> Content, - /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. - pub enum_item: fn(number: Option, body: Content) -> Content, - /// An item in a term list: `/ Term: Details`. - pub term_item: fn(term: Content, description: Content) -> Content, - /// A mathematical equation: `$x$`, `$ x^2 $`. - pub equation: fn(body: Content, block: bool) -> Content, - /// An alignment point in math: `&`. - pub math_align_point: fn() -> Content, - /// Matched delimiters in math: `[x + y]`. - pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content, - /// A base with optional attachments in math: `a_1^2`. - #[allow(clippy::type_complexity)] - pub math_attach: fn( - base: Content, - // Positioned smartly. - t: Option, - b: Option, - // Fixed positions. - tl: Option, - bl: Option, - tr: Option, - br: Option, - ) -> Content, - /// A base with an accent: `arrow(x)`. - pub math_accent: fn(base: Content, accent: char) -> Content, - /// A fraction in math: `x/2`. - pub math_frac: fn(num: Content, denom: Content) -> Content, - /// A root in math: `√x`, `∛x` or `∜x`. - pub math_root: fn(index: Option, radicand: Content) -> Content, - /// Dispatch a method on a library value. - pub library_method: fn( - vm: &mut Vm, - dynamic: &Dynamic, - method: &str, - args: Args, - span: Span, - ) -> SourceResult, -} - -impl Debug for LangItems { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("LangItems { .. }") - } -} - -impl Hash for LangItems { - fn hash(&self, state: &mut H) { - (self.layout as usize).hash(state); - (self.em as usize).hash(state); - (self.dir as usize).hash(state); - self.space.hash(state); - self.linebreak.hash(state); - self.text.hash(state); - self.text_func.hash(state); - (self.text_str as usize).hash(state); - self.smart_quote.hash(state); - self.parbreak.hash(state); - self.strong.hash(state); - self.emph.hash(state); - self.raw.hash(state); - self.raw_languages.hash(state); - self.link.hash(state); - self.reference.hash(state); - (self.bibliography_keys as usize).hash(state); - self.heading.hash(state); - self.heading_func.hash(state); - self.list_item.hash(state); - self.enum_item.hash(state); - self.term_item.hash(state); - self.equation.hash(state); - self.math_align_point.hash(state); - self.math_delimited.hash(state); - self.math_attach.hash(state); - self.math_accent.hash(state); - self.math_frac.hash(state); - self.math_root.hash(state); - (self.library_method as usize).hash(state); - } -} - -/// Global storage for lang items. -#[doc(hidden)] -pub static LANG_ITEMS: OnceLock = OnceLock::new(); - -/// Set the lang items. -/// -/// This is a hack :( -/// -/// Passing the lang items everywhere they are needed (especially text related -/// things) is very painful. By storing them globally, in theory, we break -/// incremental, but only when different sets of lang items are used in the same -/// program. For this reason, if this function is called multiple times, the -/// items must be the same (and this is enforced). -pub fn set_lang_items(items: LangItems) { - if let Err(items) = LANG_ITEMS.set(items) { - let first = hash128(LANG_ITEMS.get().unwrap()); - let second = hash128(&items); - assert_eq!(first, second, "set differing lang items"); - } -} - -/// Access a lang item. -macro_rules! item { - ($name:ident) => { - $crate::eval::LANG_ITEMS.get().unwrap().$name - }; -} diff --git a/src/eval/methods.rs b/src/eval/methods.rs deleted file mode 100644 index 62ac4095..00000000 --- a/src/eval/methods.rs +++ /dev/null @@ -1,373 +0,0 @@ -//! Methods on values. - -use ecow::EcoString; - -use super::{Args, IntoValue, Str, Value, Vm}; -use crate::diag::{At, SourceResult}; -use crate::eval::Datetime; -use crate::model::{Location, Selector}; -use crate::syntax::Span; - -/// Call a method on a value. -pub fn call( - vm: &mut Vm, - value: Value, - method: &str, - mut args: Args, - span: Span, -) -> SourceResult { - let name = value.type_name(); - let missing = || Err(missing_method(name, method)).at(span); - - let output = match value { - Value::Color(color) => match method { - "lighten" => color.lighten(args.expect("amount")?).into_value(), - "darken" => color.darken(args.expect("amount")?).into_value(), - "negate" => color.negate().into_value(), - _ => return missing(), - }, - - Value::Str(string) => match method { - "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() - } - "slice" => { - let start = args.expect("start")?; - let mut end = args.eat()?; - if end.is_none() { - end = args.named("count")?.map(|c: i64| start + c); - } - string.slice(start, end).at(span)?.into_value() - } - "clusters" => string.clusters().into_value(), - "codepoints" => string.codepoints().into_value(), - "contains" => string.contains(args.expect("pattern")?).into_value(), - "starts-with" => string.starts_with(args.expect("pattern")?).into_value(), - "ends-with" => string.ends_with(args.expect("pattern")?).into_value(), - "find" => string.find(args.expect("pattern")?).into_value(), - "position" => string.position(args.expect("pattern")?).into_value(), - "match" => string.match_(args.expect("pattern")?).into_value(), - "matches" => string.matches(args.expect("pattern")?).into_value(), - "replace" => { - let pattern = args.expect("pattern")?; - let with = args.expect("string or function")?; - let count = args.named("count")?; - string.replace(vm, pattern, with, count)?.into_value() - } - "trim" => { - let pattern = args.eat()?; - let at = args.named("at")?; - let repeat = args.named("repeat")?.unwrap_or(true); - string.trim(pattern, at, repeat).into_value() - } - "split" => string.split(args.eat()?).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(span)?, - "fields" => content.dict().into_value(), - "location" => content - .location() - .ok_or("this method can only be called on content returned by query(..)") - .at(span)? - .into_value(), - _ => return missing(), - }, - - Value::Array(array) => match method { - "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(), - "slice" => { - let start = args.expect("start")?; - let mut end = args.eat()?; - if end.is_none() { - end = args.named("count")?.map(|c: i64| start + c); - } - array.slice(start, end).at(span)?.into_value() - } - "contains" => array.contains(&args.expect("value")?).into_value(), - "find" => array.find(vm, args.expect("function")?)?.into_value(), - "position" => array.position(vm, args.expect("function")?)?.into_value(), - "filter" => array.filter(vm, args.expect("function")?)?.into_value(), - "map" => array.map(vm, args.expect("function")?)?.into_value(), - "fold" => { - array.fold(vm, args.expect("initial value")?, args.expect("function")?)? - } - "sum" => array.sum(args.named("default")?, span)?, - "product" => array.product(args.named("default")?, span)?, - "any" => array.any(vm, args.expect("function")?)?.into_value(), - "all" => array.all(vm, args.expect("function")?)?.into_value(), - "flatten" => array.flatten().into_value(), - "rev" => array.rev().into_value(), - "split" => array.split(args.expect("separator")?).into_value(), - "join" => { - let sep = args.eat()?; - let last = args.named("last")?; - array.join(sep, last).at(span)? - } - "sorted" => array.sorted(vm, span, args.named("key")?)?.into_value(), - "zip" => array.zip(args.expect("other")?).into_value(), - "enumerate" => array.enumerate().into_value(), - _ => return missing(), - }, - - Value::Dict(dict) => match method { - "len" => dict.len().into_value(), - "at" => dict - .at(&args.expect::("key")?, args.named("default")?.as_ref()) - .at(span)? - .clone(), - "keys" => dict.keys().into_value(), - "values" => dict.values().into_value(), - "pairs" => dict.pairs().into_value(), - _ => return missing(), - }, - - Value::Func(func) => match method { - "with" => func.with(args.take()).into_value(), - "where" => { - let fields = args.to_named(); - args.items.retain(|arg| arg.name.is_none()); - func.element() - .ok_or("`where()` can only be called on element functions") - .at(span)? - .where_(fields) - .into_value() - } - _ => return missing(), - }, - - Value::Args(args) => match method { - "pos" => args.to_pos().into_value(), - "named" => args.to_named().into_value(), - _ => return missing(), - }, - - Value::Dyn(dynamic) => { - if let Some(location) = dynamic.downcast::() { - match method { - "page" => vm.vt.introspector.page(*location).into_value(), - "position" => vm.vt.introspector.position(*location).into_value(), - "page-numbering" => vm.vt.introspector.page_numbering(*location), - _ => return missing(), - } - } else if let Some(selector) = dynamic.downcast::() { - match method { - "or" => selector.clone().or(args.all::()?).into_value(), - "and" => selector.clone().and(args.all::()?).into_value(), - "before" => { - let location = args.expect::("selector")?; - let inclusive = - args.named_or_find::("inclusive")?.unwrap_or(true); - selector.clone().before(location, inclusive).into_value() - } - "after" => { - let location = args.expect::("selector")?; - let inclusive = - args.named_or_find::("inclusive")?.unwrap_or(true); - selector.clone().after(location, inclusive).into_value() - } - _ => return missing(), - } - } else if let Some(&datetime) = dynamic.downcast::() { - match method { - "display" => { - datetime.display(args.eat()?).at(args.span)?.into_value() - } - "year" => datetime.year().into_value(), - "month" => datetime.month().into_value(), - "weekday" => datetime.weekday().into_value(), - "day" => datetime.day().into_value(), - "hour" => datetime.hour().into_value(), - "minute" => datetime.minute().into_value(), - "second" => datetime.second().into_value(), - _ => return missing(), - } - } else { - return (vm.items.library_method)(vm, &dynamic, method, args, span); - } - } - - _ => return missing(), - }; - - args.finish()?; - Ok(output) -} - -/// Call a mutating method on a value. -pub fn call_mut( - value: &mut Value, - method: &str, - mut args: Args, - span: Span, -) -> SourceResult { - let name = value.type_name(); - let missing = || Err(missing_method(name, method)).at(span); - let mut output = Value::None; - - match value { - Value::Array(array) => match method { - "push" => array.push(args.expect("value")?), - "pop" => output = array.pop().at(span)?, - "insert" => { - array.insert(args.expect("index")?, args.expect("value")?).at(span)? - } - "remove" => output = array.remove(args.expect("index")?).at(span)?, - _ => return missing(), - }, - - Value::Dict(dict) => match method { - "insert" => dict.insert(args.expect::("key")?, args.expect("value")?), - "remove" => { - output = dict.remove(&args.expect::("key")?).at(span)? - } - _ => return missing(), - }, - - _ => return missing(), - } - - args.finish()?; - Ok(output) -} - -/// Call an accessor method on a value. -pub fn call_access<'a>( - value: &'a mut Value, - method: &str, - mut args: Args, - span: Span, -) -> SourceResult<&'a mut Value> { - let name = value.type_name(); - let missing = || Err(missing_method(name, method)).at(span); - - let slot = match value { - Value::Array(array) => match method { - "first" => array.first_mut().at(span)?, - "last" => array.last_mut().at(span)?, - "at" => array.at_mut(args.expect("index")?).at(span)?, - _ => return missing(), - }, - Value::Dict(dict) => match method { - "at" => dict.at_mut(&args.expect::("key")?).at(span)?, - _ => return missing(), - }, - _ => return missing(), - }; - - args.finish()?; - Ok(slot) -} - -/// Whether a specific method is mutating. -pub fn is_mutating(method: &str) -> bool { - matches!(method, "push" | "pop" | "insert" | "remove") -} - -/// Whether a specific method is an accessor. -pub fn is_accessor(method: &str) -> bool { - matches!(method, "first" | "last" | "at") -} - -/// The missing method error message. -#[cold] -fn missing_method(type_name: &str, method: &str) -> String { - format!("type {type_name} has no method `{method}`") -} - -/// List the available methods for a type and whether they take arguments. -pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] { - match type_name { - "color" => &[("lighten", true), ("darken", true), ("negate", false)], - "string" => &[ - ("len", false), - ("at", true), - ("clusters", false), - ("codepoints", false), - ("contains", true), - ("ends-with", true), - ("find", true), - ("first", false), - ("last", false), - ("match", true), - ("matches", true), - ("position", true), - ("replace", true), - ("slice", true), - ("split", true), - ("starts-with", true), - ("trim", true), - ], - "content" => &[ - ("func", false), - ("has", true), - ("at", true), - ("fields", false), - ("location", false), - ], - "array" => &[ - ("all", true), - ("any", true), - ("at", true), - ("contains", true), - ("filter", true), - ("find", true), - ("first", false), - ("flatten", false), - ("fold", true), - ("insert", true), - ("split", true), - ("join", true), - ("last", false), - ("len", false), - ("map", true), - ("pop", false), - ("position", true), - ("push", true), - ("remove", true), - ("rev", false), - ("slice", true), - ("sorted", false), - ("enumerate", false), - ("zip", true), - ], - "dictionary" => &[ - ("at", true), - ("insert", true), - ("keys", false), - ("len", false), - ("pairs", false), - ("remove", true), - ("values", false), - ], - "function" => &[("where", true), ("with", true)], - "arguments" => &[("named", false), ("pos", false)], - "location" => &[("page", false), ("position", false), ("page-numbering", false)], - "selector" => &[("or", true), ("and", true), ("before", true), ("after", true)], - "counter" => &[ - ("display", true), - ("at", true), - ("final", true), - ("step", true), - ("update", true), - ], - "state" => &[("display", true), ("at", true), ("final", true), ("update", true)], - _ => &[], - } -} diff --git a/src/eval/mod.rs b/src/eval/mod.rs deleted file mode 100644 index fe28e3f3..00000000 --- a/src/eval/mod.rs +++ /dev/null @@ -1,1908 +0,0 @@ -//! Evaluation of markup into modules. - -#[macro_use] -mod library; -#[macro_use] -mod cast; -#[macro_use] -mod array; -#[macro_use] -mod dict; -#[macro_use] -mod str; -#[macro_use] -mod value; -mod args; -mod auto; -mod datetime; -mod func; -mod int; -mod methods; -mod module; -mod none; -pub mod ops; -mod scope; -mod symbol; - -#[doc(hidden)] -pub use { - self::library::LANG_ITEMS, - ecow::{eco_format, eco_vec}, - indexmap::IndexMap, - once_cell::sync::Lazy, -}; - -#[doc(inline)] -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::cast::{ - cast, Cast, CastInfo, FromValue, IntoResult, IntoValue, Never, Reflect, Variadics, -}; -pub use self::datetime::Datetime; -pub use self::dict::{dict, Dict}; -pub use self::func::{Func, FuncInfo, NativeFunc, Param, ParamInfo}; -pub use self::library::{set_lang_items, LangItems, Library}; -pub use self::methods::methods_on; -pub use self::module::Module; -pub use self::none::NoneValue; -pub use self::scope::{Scope, Scopes}; -pub use self::str::{format_str, Regex, Str}; -pub use self::symbol::Symbol; -pub use self::value::{Dynamic, Type, Value}; - -use std::collections::HashSet; -use std::mem; -use std::path::Path; - -use comemo::{Track, Tracked, TrackedMut, Validate}; -use ecow::{EcoString, EcoVec}; -use unicode_segmentation::UnicodeSegmentation; - -use self::func::{CapturesVisitor, Closure}; -use crate::diag::{ - bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, -}; -use crate::file::{FileId, PackageManifest, PackageSpec}; -use crate::model::{ - Content, DelayedErrors, Introspector, Label, Locator, Recipe, ShowableSelector, - Styles, Transform, Unlabellable, Vt, -}; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::{parse_code, Source, Span, Spanned, SyntaxKind, SyntaxNode}; -use crate::World; - -const MAX_ITERATIONS: usize = 10_000; -const MAX_CALL_DEPTH: usize = 64; - -/// Evaluate a source file and return the resulting module. -#[comemo::memoize] -#[tracing::instrument(skip(world, route, tracer, source))] -pub fn eval( - world: Tracked, - route: Tracked, - tracer: TrackedMut, - source: &Source, -) -> SourceResult { - // Prevent cyclic evaluation. - let id = source.id(); - if route.contains(id) { - panic!("Tried to cyclicly evaluate {}", id.path().display()); - } - - // Hook up the lang items. - let library = world.library(); - set_lang_items(library.items.clone()); - - // Prepare VT. - let mut locator = Locator::default(); - let introspector = Introspector::default(); - let mut delayed = DelayedErrors::default(); - let vt = Vt { - world, - introspector: introspector.track(), - locator: &mut locator, - delayed: delayed.track_mut(), - tracer, - }; - - // Prepare VM. - let route = Route::insert(route, id); - let scopes = Scopes::new(Some(library)); - let mut vm = Vm::new(vt, route.track(), id, scopes); - let root = match source.root().cast::() { - Some(markup) if vm.traced.is_some() => markup, - _ => source.ast()?, - }; - - // Evaluate the module. - let result = root.eval(&mut vm); - - // Handle control flow. - if let Some(flow) = vm.flow { - bail!(flow.forbidden()); - } - - // Assemble the module. - let name = id.path().file_stem().unwrap_or_default().to_string_lossy(); - Ok(Module::new(name).with_scope(vm.scopes.top).with_content(result?)) -} - -/// Evaluate a string as code and return the resulting value. -/// -/// Everything in the output is associated with the given `span`. -#[comemo::memoize] -pub fn eval_string( - world: Tracked, - code: &str, - span: Span, -) -> SourceResult { - let mut root = parse_code(code); - root.synthesize(span); - - let errors = root.errors(); - if !errors.is_empty() { - return Err(Box::new(errors)); - } - - // Prepare VT. - let mut tracer = Tracer::default(); - let mut locator = Locator::default(); - let mut delayed = DelayedErrors::default(); - let introspector = Introspector::default(); - let vt = Vt { - world, - introspector: introspector.track(), - locator: &mut locator, - delayed: delayed.track_mut(), - tracer: tracer.track_mut(), - }; - - // Prepare VM. - let route = Route::default(); - let id = FileId::detached(); - let scopes = Scopes::new(Some(world.library())); - let mut vm = Vm::new(vt, route.track(), id, scopes); - - // Evaluate the code. - let code = root.cast::().unwrap(); - let result = code.eval(&mut vm); - - // Handle control flow. - if let Some(flow) = vm.flow { - bail!(flow.forbidden()); - } - - result -} - -/// A virtual machine. -/// -/// Holds the state needed to [evaluate](eval) Typst sources. A new -/// virtual machine is created for each module evaluation and function call. -pub struct Vm<'a> { - /// The underlying virtual typesetter. - pub vt: Vt<'a>, - /// The language items. - items: LangItems, - /// The route of source ids the VM took to reach its current location. - route: Tracked<'a, Route<'a>>, - /// The current location. - location: FileId, - /// A control flow event that is currently happening. - flow: Option, - /// The stack of scopes. - scopes: Scopes<'a>, - /// The current call depth. - depth: usize, - /// A span that is currently traced. - traced: Option, -} - -impl<'a> Vm<'a> { - /// Create a new virtual machine. - fn new( - vt: Vt<'a>, - route: Tracked<'a, Route>, - location: FileId, - scopes: Scopes<'a>, - ) -> Self { - let traced = vt.tracer.span(location); - let items = vt.world.library().items.clone(); - Self { - vt, - items, - route, - location, - flow: None, - scopes, - depth: 0, - traced, - } - } - - /// Access the underlying world. - pub fn world(&self) -> Tracked<'a, dyn World + 'a> { - self.vt.world - } - - /// The location to which paths are relative currently. - pub fn location(&self) -> FileId { - self.location - } - - /// Define a variable in the current scope. - #[tracing::instrument(skip_all)] - pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { - let value = value.into_value(); - if self.traced == Some(var.span()) { - self.vt.tracer.trace(value.clone()); - } - self.scopes.top.define(var.take(), value); - } -} - -/// A control flow event that occurred during evaluation. -#[derive(Debug, Clone, PartialEq)] -pub enum FlowEvent { - /// Stop iteration in a loop. - Break(Span), - /// Skip the remainder of the current iteration in a loop. - Continue(Span), - /// Stop execution of a function early, optionally returning an explicit - /// value. - Return(Span, Option), -} - -impl FlowEvent { - /// Return an error stating that this control flow is forbidden. - pub fn forbidden(&self) -> SourceError { - match *self { - Self::Break(span) => { - error!(span, "cannot break outside of loop") - } - Self::Continue(span) => { - error!(span, "cannot continue outside of loop") - } - Self::Return(span, _) => { - error!(span, "cannot return outside of function") - } - } - } -} - -/// A route of source ids. -#[derive(Default)] -pub struct Route<'a> { - // We need to override the constraint's lifetime here so that `Tracked` is - // covariant over the constraint. If it becomes invariant, we're in for a - // world of lifetime pain. - outer: Option as Validate>::Constraint>>, - id: Option, -} - -impl<'a> Route<'a> { - /// Create a new route with just one entry. - pub fn new(id: FileId) -> Self { - Self { id: Some(id), outer: None } - } - - /// Insert a new id into the route. - /// - /// You must guarantee that `outer` lives longer than the resulting - /// route is ever used. - pub fn insert(outer: Tracked<'a, Self>, id: FileId) -> Self { - Route { outer: Some(outer), id: Some(id) } - } - - /// Start tracking this locator. - /// - /// In comparison to [`Track::track`], this method skips this chain link - /// if it does not contribute anything. - pub fn track(&self) -> Tracked<'_, Self> { - match self.outer { - Some(outer) if self.id.is_none() => outer, - _ => Track::track(self), - } - } -} - -#[comemo::track] -impl<'a> Route<'a> { - /// Whether the given id is part of the route. - fn contains(&self, id: FileId) -> bool { - self.id == Some(id) || self.outer.map_or(false, |outer| outer.contains(id)) - } -} - -/// Traces which values existed for an expression at a span. -#[derive(Default, Clone)] -pub struct Tracer { - span: Option, - values: Vec, -} - -impl Tracer { - /// The maximum number of traced items. - pub const MAX: usize = 10; - - /// Create a new tracer, possibly with a span under inspection. - pub fn new(span: Option) -> Self { - Self { span, values: vec![] } - } - - /// Get the traced values. - pub fn finish(self) -> Vec { - self.values - } -} - -#[comemo::track] -impl Tracer { - /// The traced span if it is part of the given source file. - fn span(&self, id: FileId) -> Option { - if self.span.map(Span::id) == Some(id) { - self.span - } else { - None - } - } - - /// Trace a value for the span. - fn trace(&mut self, v: Value) { - if self.values.len() < Self::MAX { - self.values.push(v); - } - } -} - -/// Evaluate an expression. -pub(super) trait Eval { - /// The output of evaluating the expression. - type Output; - - /// Evaluate the expression to the output value. - fn eval(&self, vm: &mut Vm) -> SourceResult; -} - -impl Eval for ast::Markup { - type Output = Content; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - eval_markup(vm, &mut self.exprs()) - } -} - -/// Evaluate a stream of markup. -fn eval_markup( - vm: &mut Vm, - exprs: &mut impl Iterator, -) -> SourceResult { - let flow = vm.flow.take(); - let mut seq = Vec::with_capacity(exprs.size_hint().1.unwrap_or_default()); - - while let Some(expr) = exprs.next() { - match expr { - ast::Expr::Set(set) => { - let styles = set.eval(vm)?; - if vm.flow.is_some() { - break; - } - - seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) - } - ast::Expr::Show(show) => { - let recipe = show.eval(vm)?; - if vm.flow.is_some() { - break; - } - - let tail = eval_markup(vm, exprs)?; - seq.push(tail.styled_with_recipe(vm, recipe)?) - } - expr => match expr.eval(vm)? { - Value::Label(label) => { - if let Some(elem) = - seq.iter_mut().rev().find(|node| !node.can::()) - { - *elem = mem::take(elem).labelled(label); - } - } - value => seq.push(value.display().spanned(expr.span())), - }, - } - - if vm.flow.is_some() { - break; - } - } - - if flow.is_some() { - vm.flow = flow; - } - - Ok(Content::sequence(seq)) -} - -impl Eval for ast::Expr { - type Output = Value; - - #[tracing::instrument(name = "Expr::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let span = self.span(); - let forbidden = |name| { - error!(span, "{} is only allowed directly in code and content blocks", name) - }; - - let v = match self { - Self::Text(v) => v.eval(vm).map(Value::Content), - Self::Space(v) => v.eval(vm).map(Value::Content), - Self::Linebreak(v) => v.eval(vm).map(Value::Content), - Self::Parbreak(v) => v.eval(vm).map(Value::Content), - Self::Escape(v) => v.eval(vm), - Self::Shorthand(v) => v.eval(vm), - Self::SmartQuote(v) => v.eval(vm).map(Value::Content), - Self::Strong(v) => v.eval(vm).map(Value::Content), - Self::Emph(v) => v.eval(vm).map(Value::Content), - Self::Raw(v) => v.eval(vm).map(Value::Content), - Self::Link(v) => v.eval(vm).map(Value::Content), - Self::Label(v) => v.eval(vm), - Self::Ref(v) => v.eval(vm).map(Value::Content), - Self::Heading(v) => v.eval(vm).map(Value::Content), - Self::List(v) => v.eval(vm).map(Value::Content), - Self::Enum(v) => v.eval(vm).map(Value::Content), - Self::Term(v) => v.eval(vm).map(Value::Content), - Self::Equation(v) => v.eval(vm).map(Value::Content), - Self::Math(v) => v.eval(vm).map(Value::Content), - Self::MathIdent(v) => v.eval(vm), - Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), - Self::MathDelimited(v) => v.eval(vm).map(Value::Content), - Self::MathAttach(v) => v.eval(vm).map(Value::Content), - Self::MathFrac(v) => v.eval(vm).map(Value::Content), - Self::MathRoot(v) => v.eval(vm).map(Value::Content), - Self::Ident(v) => v.eval(vm), - Self::None(v) => v.eval(vm), - Self::Auto(v) => v.eval(vm), - Self::Bool(v) => v.eval(vm), - Self::Int(v) => v.eval(vm), - Self::Float(v) => v.eval(vm), - Self::Numeric(v) => v.eval(vm), - Self::Str(v) => v.eval(vm), - Self::Code(v) => v.eval(vm), - Self::Content(v) => v.eval(vm).map(Value::Content), - Self::Array(v) => v.eval(vm).map(Value::Array), - Self::Dict(v) => v.eval(vm).map(Value::Dict), - Self::Parenthesized(v) => v.eval(vm), - Self::FieldAccess(v) => v.eval(vm), - Self::FuncCall(v) => v.eval(vm), - Self::Closure(v) => v.eval(vm), - Self::Unary(v) => v.eval(vm), - Self::Binary(v) => v.eval(vm), - Self::Let(v) => v.eval(vm), - Self::DestructAssign(v) => v.eval(vm), - Self::Set(_) => bail!(forbidden("set")), - Self::Show(_) => bail!(forbidden("show")), - Self::Conditional(v) => v.eval(vm), - Self::While(v) => v.eval(vm), - Self::For(v) => v.eval(vm), - Self::Import(v) => v.eval(vm), - Self::Include(v) => v.eval(vm).map(Value::Content), - Self::Break(v) => v.eval(vm), - Self::Continue(v) => v.eval(vm), - Self::Return(v) => v.eval(vm), - }? - .spanned(span); - - if vm.traced == Some(span) { - vm.vt.tracer.trace(v.clone()); - } - - Ok(v) - } -} - -impl ast::Expr { - fn eval_display(&self, vm: &mut Vm) -> SourceResult { - Ok(self.eval(vm)?.display().spanned(self.span())) - } -} - -impl Eval for ast::Text { - type Output = Content; - - #[tracing::instrument(name = "Text::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.text)(self.get().clone())) - } -} - -impl Eval for ast::Space { - type Output = Content; - - #[tracing::instrument(name = "Space::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.space)()) - } -} - -impl Eval for ast::Linebreak { - type Output = Content; - - #[tracing::instrument(name = "Linebreak::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.linebreak)()) - } -} - -impl Eval for ast::Parbreak { - type Output = Content; - - #[tracing::instrument(name = "Parbreak::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.parbreak)()) - } -} - -impl Eval for ast::Escape { - type Output = Value; - - #[tracing::instrument(name = "Escape::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::Symbol(Symbol::new(self.get()))) - } -} - -impl Eval for ast::Shorthand { - type Output = Value; - - #[tracing::instrument(name = "Shorthand::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::Symbol(Symbol::new(self.get()))) - } -} - -impl Eval for ast::SmartQuote { - type Output = Content; - - #[tracing::instrument(name = "SmartQuote::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.smart_quote)(self.double())) - } -} - -impl Eval for ast::Strong { - type Output = Content; - - #[tracing::instrument(name = "Strong::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.strong)(self.body().eval(vm)?)) - } -} - -impl Eval for ast::Emph { - type Output = Content; - - #[tracing::instrument(name = "Emph::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.emph)(self.body().eval(vm)?)) - } -} - -impl Eval for ast::Raw { - type Output = Content; - - #[tracing::instrument(name = "Raw::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let text = self.text(); - let lang = self.lang().map(Into::into); - let block = self.block(); - Ok((vm.items.raw)(text, lang, block)) - } -} - -impl Eval for ast::Link { - type Output = Content; - - #[tracing::instrument(name = "Link::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.link)(self.get().clone())) - } -} - -impl Eval for ast::Label { - type Output = Value; - - #[tracing::instrument(name = "Label::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::Label(Label(self.get().into()))) - } -} - -impl Eval for ast::Ref { - type Output = Content; - - #[tracing::instrument(name = "Ref::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let label = Label(self.target().into()); - let supplement = self.supplement().map(|block| block.eval(vm)).transpose()?; - Ok((vm.items.reference)(label, supplement)) - } -} - -impl Eval for ast::Heading { - type Output = Content; - - #[tracing::instrument(name = "Heading::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let level = self.level(); - let body = self.body().eval(vm)?; - Ok((vm.items.heading)(level, body)) - } -} - -impl Eval for ast::ListItem { - type Output = Content; - - #[tracing::instrument(name = "ListItem::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.list_item)(self.body().eval(vm)?)) - } -} - -impl Eval for ast::EnumItem { - type Output = Content; - - #[tracing::instrument(name = "EnumItem::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let number = self.number(); - let body = self.body().eval(vm)?; - Ok((vm.items.enum_item)(number, body)) - } -} - -impl Eval for ast::TermItem { - type Output = Content; - - #[tracing::instrument(name = "TermItem::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let term = self.term().eval(vm)?; - let description = self.description().eval(vm)?; - Ok((vm.items.term_item)(term, description)) - } -} - -impl Eval for ast::Equation { - type Output = Content; - - #[tracing::instrument(name = "Equation::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let body = self.body().eval(vm)?; - let block = self.block(); - Ok((vm.items.equation)(body, block)) - } -} - -impl Eval for ast::Math { - type Output = Content; - - #[tracing::instrument(name = "Math::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok(Content::sequence( - self.exprs() - .map(|expr| expr.eval_display(vm)) - .collect::>>()?, - )) - } -} - -impl Eval for ast::MathIdent { - type Output = Value; - - #[tracing::instrument(name = "MathIdent::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - vm.scopes.get_in_math(self).cloned().at(self.span()) - } -} - -impl Eval for ast::MathAlignPoint { - type Output = Content; - - #[tracing::instrument(name = "MathAlignPoint::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.math_align_point)()) - } -} - -impl Eval for ast::MathDelimited { - type Output = Content; - - #[tracing::instrument(name = "MathDelimited::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let open = self.open().eval_display(vm)?; - let body = self.body().eval(vm)?; - let close = self.close().eval_display(vm)?; - Ok((vm.items.math_delimited)(open, body, close)) - } -} - -impl Eval for ast::MathAttach { - type Output = Content; - - #[tracing::instrument(name = "MathAttach::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let base = self.base().eval_display(vm)?; - let top = self.top().map(|expr| expr.eval_display(vm)).transpose()?; - let bottom = self.bottom().map(|expr| expr.eval_display(vm)).transpose()?; - Ok((vm.items.math_attach)(base, top, bottom, None, None, None, None)) - } -} - -impl Eval for ast::MathFrac { - type Output = Content; - - #[tracing::instrument(name = "MathFrac::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let num = self.num().eval_display(vm)?; - let denom = self.denom().eval_display(vm)?; - Ok((vm.items.math_frac)(num, denom)) - } -} - -impl Eval for ast::MathRoot { - type Output = Content; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - let index = self.index().map(|i| (vm.items.text)(eco_format!("{i}"))); - let radicand = self.radicand().eval_display(vm)?; - Ok((vm.items.math_root)(index, radicand)) - } -} - -impl Eval for ast::Ident { - type Output = Value; - - #[tracing::instrument(name = "Ident::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - vm.scopes.get(self).cloned().at(self.span()) - } -} - -impl Eval for ast::None { - type Output = Value; - - #[tracing::instrument(name = "None::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::None) - } -} - -impl Eval for ast::Auto { - type Output = Value; - - #[tracing::instrument(name = "Auto::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::Auto) - } -} - -impl Eval for ast::Bool { - type Output = Value; - - #[tracing::instrument(name = "Bool::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::Bool(self.get())) - } -} - -impl Eval for ast::Int { - type Output = Value; - - #[tracing::instrument(name = "Int::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::Int(self.get())) - } -} - -impl Eval for ast::Float { - type Output = Value; - - #[tracing::instrument(name = "Float::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::Float(self.get())) - } -} - -impl Eval for ast::Numeric { - type Output = Value; - - #[tracing::instrument(name = "Numeric::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::numeric(self.get())) - } -} - -impl Eval for ast::Str { - type Output = Value; - - #[tracing::instrument(name = "Str::eval", skip_all)] - fn eval(&self, _: &mut Vm) -> SourceResult { - Ok(Value::Str(self.get().into())) - } -} - -impl Eval for ast::CodeBlock { - type Output = Value; - - #[tracing::instrument(name = "CodeBlock::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - vm.scopes.enter(); - let output = self.body().eval(vm)?; - vm.scopes.exit(); - Ok(output) - } -} - -impl Eval for ast::Code { - type Output = Value; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - eval_code(vm, &mut self.exprs()) - } -} - -/// Evaluate a stream of expressions. -fn eval_code( - vm: &mut Vm, - exprs: &mut impl Iterator, -) -> SourceResult { - let flow = vm.flow.take(); - let mut output = Value::None; - - while let Some(expr) = exprs.next() { - let span = expr.span(); - let value = match expr { - ast::Expr::Set(set) => { - let styles = set.eval(vm)?; - if vm.flow.is_some() { - break; - } - - let tail = eval_code(vm, exprs)?.display(); - Value::Content(tail.styled_with_map(styles)) - } - ast::Expr::Show(show) => { - let recipe = show.eval(vm)?; - if vm.flow.is_some() { - break; - } - - let tail = eval_code(vm, exprs)?.display(); - Value::Content(tail.styled_with_recipe(vm, recipe)?) - } - _ => expr.eval(vm)?, - }; - - output = ops::join(output, value).at(span)?; - - if vm.flow.is_some() { - break; - } - } - - if flow.is_some() { - vm.flow = flow; - } - - Ok(output) -} - -impl Eval for ast::ContentBlock { - type Output = Content; - - #[tracing::instrument(name = "ContentBlock::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - vm.scopes.enter(); - let content = self.body().eval(vm)?; - vm.scopes.exit(); - Ok(content) - } -} - -impl Eval for ast::Parenthesized { - type Output = Value; - - #[tracing::instrument(name = "Parenthesized::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - self.expr().eval(vm) - } -} - -impl Eval for ast::Array { - type Output = Array; - - #[tracing::instrument(skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let items = self.items(); - - let mut vec = EcoVec::with_capacity(items.size_hint().0); - for item in items { - match item { - ast::ArrayItem::Pos(expr) => vec.push(expr.eval(vm)?), - ast::ArrayItem::Spread(expr) => match expr.eval(vm)? { - Value::None => {} - Value::Array(array) => vec.extend(array.into_iter()), - v => bail!(expr.span(), "cannot spread {} into array", v.type_name()), - }, - } - } - - Ok(vec.into()) - } -} - -impl Eval for ast::Dict { - type Output = Dict; - - #[tracing::instrument(skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let mut map = indexmap::IndexMap::new(); - - for item in self.items() { - match item { - ast::DictItem::Named(named) => { - map.insert(named.name().take().into(), named.expr().eval(vm)?); - } - ast::DictItem::Keyed(keyed) => { - map.insert(keyed.key().get().into(), keyed.expr().eval(vm)?); - } - ast::DictItem::Spread(expr) => match expr.eval(vm)? { - Value::None => {} - Value::Dict(dict) => map.extend(dict.into_iter()), - v => bail!( - expr.span(), - "cannot spread {} into dictionary", - v.type_name() - ), - }, - } - } - - Ok(map.into()) - } -} - -impl Eval for ast::Unary { - type Output = Value; - - #[tracing::instrument(name = "Unary::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let value = self.expr().eval(vm)?; - let result = match self.op() { - ast::UnOp::Pos => ops::pos(value), - ast::UnOp::Neg => ops::neg(value), - ast::UnOp::Not => ops::not(value), - }; - result.at(self.span()) - } -} - -impl Eval for ast::Binary { - type Output = Value; - - #[tracing::instrument(name = "Binary::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - match self.op() { - ast::BinOp::Add => self.apply(vm, ops::add), - ast::BinOp::Sub => self.apply(vm, ops::sub), - ast::BinOp::Mul => self.apply(vm, ops::mul), - ast::BinOp::Div => self.apply(vm, ops::div), - ast::BinOp::And => self.apply(vm, ops::and), - ast::BinOp::Or => self.apply(vm, ops::or), - ast::BinOp::Eq => self.apply(vm, ops::eq), - ast::BinOp::Neq => self.apply(vm, ops::neq), - ast::BinOp::Lt => self.apply(vm, ops::lt), - ast::BinOp::Leq => self.apply(vm, ops::leq), - ast::BinOp::Gt => self.apply(vm, ops::gt), - ast::BinOp::Geq => self.apply(vm, ops::geq), - ast::BinOp::In => self.apply(vm, ops::in_), - ast::BinOp::NotIn => self.apply(vm, ops::not_in), - ast::BinOp::Assign => self.assign(vm, |_, b| Ok(b)), - ast::BinOp::AddAssign => self.assign(vm, ops::add), - ast::BinOp::SubAssign => self.assign(vm, ops::sub), - ast::BinOp::MulAssign => self.assign(vm, ops::mul), - ast::BinOp::DivAssign => self.assign(vm, ops::div), - } - } -} - -impl ast::Binary { - /// Apply a basic binary operation. - fn apply( - &self, - vm: &mut Vm, - op: fn(Value, Value) -> StrResult, - ) -> SourceResult { - let lhs = self.lhs().eval(vm)?; - - // Short-circuit boolean operations. - if (self.op() == ast::BinOp::And && lhs == Value::Bool(false)) - || (self.op() == ast::BinOp::Or && lhs == Value::Bool(true)) - { - return Ok(lhs); - } - - let rhs = self.rhs().eval(vm)?; - op(lhs, rhs).at(self.span()) - } - - /// Apply an assignment operation. - fn assign( - &self, - vm: &mut Vm, - op: fn(Value, Value) -> StrResult, - ) -> SourceResult { - let rhs = self.rhs().eval(vm)?; - let lhs = self.lhs(); - - // An assignment to a dictionary field is different from a normal access - // since it can create the field instead of just modifying it. - if self.op() == ast::BinOp::Assign { - if let ast::Expr::FieldAccess(access) = &lhs { - let dict = access.access_dict(vm)?; - dict.insert(access.field().take().into(), rhs); - return Ok(Value::None); - } - } - - let location = self.lhs().access(vm)?; - let lhs = std::mem::take(&mut *location); - *location = op(lhs, rhs).at(self.span())?; - Ok(Value::None) - } -} - -impl Eval for ast::FieldAccess { - type Output = Value; - - #[tracing::instrument(name = "FieldAccess::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let value = self.target().eval(vm)?; - let field = self.field(); - value.field(&field).at(field.span()) - } -} - -impl Eval for ast::FuncCall { - type Output = Value; - - #[tracing::instrument(name = "FuncCall::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let span = self.span(); - if vm.depth >= MAX_CALL_DEPTH { - bail!(span, "maximum function call depth exceeded"); - } - - let callee = self.callee(); - let in_math = in_math(&callee); - let callee_span = callee.span(); - let args = self.args(); - - // Try to evaluate as a method call. This is possible if the callee is a - // field access and does not evaluate to a module. - let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { - let target = access.target(); - let field = access.field(); - let field_span = field.span(); - let field = field.take(); - let point = || Tracepoint::Call(Some(field.clone())); - if methods::is_mutating(&field) { - let args = args.eval(vm)?; - let target = target.access(vm)?; - - // Prioritize a function's own methods (with, where) over its - // fields. This is fine as we define each field of a function, - // if it has any. - // ('methods_on' will be empty for Symbol and Module - their - // method calls always refer to their fields.) - if !matches!(target, Value::Symbol(_) | Value::Module(_) | Value::Func(_)) - || methods_on(target.type_name()).iter().any(|(m, _)| m == &field) - { - return methods::call_mut(target, &field, args, span).trace( - vm.world(), - point, - span, - ); - } - (target.field(&field).at(field_span)?, args) - } else { - let target = target.eval(vm)?; - let args = args.eval(vm)?; - - if !matches!(target, Value::Symbol(_) | Value::Module(_) | Value::Func(_)) - || methods_on(target.type_name()).iter().any(|(m, _)| m == &field) - { - return methods::call(vm, target, &field, args, span).trace( - vm.world(), - point, - span, - ); - } - (target.field(&field).at(field_span)?, args) - } - } else { - (callee.eval(vm)?, args.eval(vm)?) - }; - - // Handle math special cases for non-functions: - // Combining accent symbols apply themselves while everything else - // simply displays the arguments verbatim. - if in_math && !matches!(callee, Value::Func(_)) { - if let Value::Symbol(sym) = &callee { - let c = sym.get(); - if let Some(accent) = Symbol::combining_accent(c) { - let base = args.expect("base")?; - args.finish()?; - return Ok(Value::Content((vm.items.math_accent)(base, accent))); - } - } - let mut body = Content::empty(); - for (i, arg) in args.all::()?.into_iter().enumerate() { - if i > 0 { - body += (vm.items.text)(','.into()); - } - body += arg; - } - return Ok(Value::Content( - callee.display().spanned(callee_span) - + (vm.items.math_delimited)( - (vm.items.text)('('.into()), - body, - (vm.items.text)(')'.into()), - ), - )); - } - - let callee = callee.cast::().at(callee_span)?; - let point = || Tracepoint::Call(callee.name().map(Into::into)); - let f = || callee.call_vm(vm, args).trace(vm.world(), point, span); - - // Stacker is broken on WASM. - #[cfg(target_arch = "wasm32")] - return f(); - - #[cfg(not(target_arch = "wasm32"))] - stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, f) - } -} - -fn in_math(expr: &ast::Expr) -> bool { - match expr { - ast::Expr::MathIdent(_) => true, - ast::Expr::FieldAccess(access) => in_math(&access.target()), - _ => false, - } -} - -impl Eval for ast::Args { - type Output = Args; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - let mut items = EcoVec::new(); - - for arg in self.items() { - let span = arg.span(); - match arg { - ast::Arg::Pos(expr) => { - items.push(Arg { - span, - name: None, - value: Spanned::new(expr.eval(vm)?, expr.span()), - }); - } - ast::Arg::Named(named) => { - items.push(Arg { - span, - name: Some(named.name().take().into()), - value: Spanned::new(named.expr().eval(vm)?, named.expr().span()), - }); - } - ast::Arg::Spread(expr) => match expr.eval(vm)? { - Value::None => {} - Value::Array(array) => { - items.extend(array.into_iter().map(|value| Arg { - span, - name: None, - value: Spanned::new(value, span), - })); - } - Value::Dict(dict) => { - items.extend(dict.into_iter().map(|(key, value)| Arg { - span, - name: Some(key), - value: Spanned::new(value, span), - })); - } - Value::Args(args) => items.extend(args.items), - v => bail!(expr.span(), "cannot spread {}", v.type_name()), - }, - } - } - - Ok(Args { span: self.span(), items }) - } -} - -impl Eval for ast::Closure { - type Output = Value; - - #[tracing::instrument(name = "Closure::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - // The closure's name is defined by its let binding if there's one. - let name = self.name(); - - // Collect captured variables. - let captured = { - let mut visitor = CapturesVisitor::new(&vm.scopes); - visitor.visit(self.as_untyped()); - visitor.finish() - }; - - // Collect parameters and an optional sink parameter. - let mut params = Vec::new(); - for param in self.params().children() { - match param { - ast::Param::Pos(pattern) => params.push(Param::Pos(pattern)), - ast::Param::Named(named) => { - params.push(Param::Named(named.name(), named.expr().eval(vm)?)); - } - ast::Param::Sink(spread) => params.push(Param::Sink(spread.name())), - } - } - - // Define the closure. - let closure = Closure { - location: vm.location, - name, - captured, - params, - body: self.body(), - }; - - Ok(Value::Func(Func::from(closure).spanned(self.params().span()))) - } -} - -impl ast::Pattern { - fn destruct_array( - &self, - vm: &mut Vm, - value: Array, - f: F, - destruct: &ast::Destructuring, - ) -> SourceResult - where - F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult, - { - let mut i = 0; - let len = value.as_slice().len(); - for p in destruct.bindings() { - match p { - ast::DestructuringKind::Normal(expr) => { - let Ok(v) = value.at(i as i64, None) else { - bail!(expr.span(), "not enough elements to destructure"); - }; - f(vm, expr, v.clone())?; - i += 1; - } - ast::DestructuringKind::Sink(spread) => { - let sink_size = (1 + len).checked_sub(destruct.bindings().count()); - let sink = sink_size.and_then(|s| value.as_slice().get(i..i + s)); - if let (Some(sink_size), Some(sink)) = (sink_size, sink) { - if let Some(expr) = spread.expr() { - f(vm, expr, Value::Array(sink.into()))?; - } - i += sink_size; - } else { - bail!(self.span(), "not enough elements to destructure") - } - } - ast::DestructuringKind::Named(named) => { - bail!(named.span(), "cannot destructure named elements from an array") - } - ast::DestructuringKind::Placeholder(underscore) => { - if i < len { - i += 1 - } else { - bail!(underscore.span(), "not enough elements to destructure") - } - } - } - } - if i < len { - bail!(self.span(), "too many elements to destructure"); - } - - Ok(Value::None) - } - - fn destruct_dict( - &self, - vm: &mut Vm, - dict: Dict, - f: F, - destruct: &ast::Destructuring, - ) -> SourceResult - where - F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult, - { - let mut sink = None; - let mut used = HashSet::new(); - for p in destruct.bindings() { - match p { - ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => { - let v = dict - .at(&ident, None) - .map_err(|_| "destructuring key not found in dictionary") - .at(ident.span())?; - f(vm, ast::Expr::Ident(ident.clone()), v.clone())?; - used.insert(ident.take()); - } - ast::DestructuringKind::Sink(spread) => sink = spread.expr(), - ast::DestructuringKind::Named(named) => { - let name = named.name(); - let v = dict - .at(&name, None) - .map_err(|_| "destructuring key not found in dictionary") - .at(name.span())?; - f(vm, named.expr(), v.clone())?; - used.insert(name.take()); - } - ast::DestructuringKind::Placeholder(_) => {} - ast::DestructuringKind::Normal(expr) => { - bail!(expr.span(), "expected key, found expression"); - } - } - } - - if let Some(expr) = sink { - let mut sink = Dict::new(); - for (key, value) in dict { - if !used.contains(key.as_str()) { - sink.insert(key, value); - } - } - f(vm, expr, Value::Dict(sink))?; - } - - Ok(Value::None) - } - - /// Destruct the given value into the pattern and apply the function to each binding. - #[tracing::instrument(skip_all)] - fn apply(&self, vm: &mut Vm, value: Value, f: T) -> SourceResult - where - T: Fn(&mut Vm, ast::Expr, Value) -> SourceResult, - { - match self { - ast::Pattern::Normal(expr) => { - f(vm, expr.clone(), value)?; - Ok(Value::None) - } - ast::Pattern::Placeholder(_) => Ok(Value::None), - ast::Pattern::Destructuring(destruct) => match value { - Value::Array(value) => self.destruct_array(vm, value, f, destruct), - Value::Dict(value) => self.destruct_dict(vm, value, f, destruct), - _ => bail!(self.span(), "cannot destructure {}", value.type_name()), - }, - } - } - - /// Destruct the value into the pattern by binding. - pub fn define(&self, vm: &mut Vm, value: Value) -> SourceResult { - self.apply(vm, value, |vm, expr, value| match expr { - ast::Expr::Ident(ident) => { - vm.define(ident, value); - Ok(Value::None) - } - _ => bail!(expr.span(), "nested patterns are currently not supported"), - }) - } - - /// Destruct the value into the pattern by assignment. - pub fn assign(&self, vm: &mut Vm, value: Value) -> SourceResult { - self.apply(vm, value, |vm, expr, value| { - let location = expr.access(vm)?; - *location = value; - Ok(Value::None) - }) - } -} - -impl Eval for ast::LetBinding { - type Output = Value; - - #[tracing::instrument(name = "LetBinding::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let value = match self.init() { - Some(expr) => expr.eval(vm)?, - None => Value::None, - }; - - match self.kind() { - ast::LetBindingKind::Normal(pattern) => pattern.define(vm, value), - ast::LetBindingKind::Closure(ident) => { - vm.define(ident, value); - Ok(Value::None) - } - } - } -} - -impl Eval for ast::DestructAssignment { - type Output = Value; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - let value = self.value().eval(vm)?; - self.pattern().assign(vm, value)?; - Ok(Value::None) - } -} - -impl Eval for ast::SetRule { - type Output = Styles; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - if let Some(condition) = self.condition() { - if !condition.eval(vm)?.cast::().at(condition.span())? { - return Ok(Styles::new()); - } - } - - let target = self.target(); - let target = target - .eval(vm)? - .cast::() - .and_then(|func| { - func.element().ok_or_else(|| { - "only element functions can be used in set rules".into() - }) - }) - .at(target.span())?; - let args = self.args().eval(vm)?; - Ok(target.set(args)?.spanned(self.span())) - } -} - -impl Eval for ast::ShowRule { - type Output = Recipe; - - fn eval(&self, vm: &mut Vm) -> SourceResult { - let selector = self - .selector() - .map(|sel| sel.eval(vm)?.cast::().at(sel.span())) - .transpose()? - .map(|selector| selector.0); - - let transform = self.transform(); - let span = transform.span(); - - let transform = match transform { - ast::Expr::Set(set) => Transform::Style(set.eval(vm)?), - expr => expr.eval(vm)?.cast::().at(span)?, - }; - - Ok(Recipe { span, selector, transform }) - } -} - -impl Eval for ast::Conditional { - type Output = Value; - - #[tracing::instrument(name = "Conditional::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let condition = self.condition(); - if condition.eval(vm)?.cast::().at(condition.span())? { - self.if_body().eval(vm) - } else if let Some(else_body) = self.else_body() { - else_body.eval(vm) - } else { - Ok(Value::None) - } - } -} - -impl Eval for ast::WhileLoop { - type Output = Value; - - #[tracing::instrument(name = "WhileLoop::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let flow = vm.flow.take(); - let mut output = Value::None; - let mut i = 0; - - let condition = self.condition(); - let body = self.body(); - - while condition.eval(vm)?.cast::().at(condition.span())? { - if i == 0 - && is_invariant(condition.as_untyped()) - && !can_diverge(body.as_untyped()) - { - bail!(condition.span(), "condition is always true"); - } else if i >= MAX_ITERATIONS { - bail!(self.span(), "loop seems to be infinite"); - } - - let value = body.eval(vm)?; - output = ops::join(output, value).at(body.span())?; - - match vm.flow { - Some(FlowEvent::Break(_)) => { - vm.flow = None; - break; - } - Some(FlowEvent::Continue(_)) => vm.flow = None, - Some(FlowEvent::Return(..)) => break, - None => {} - } - - i += 1; - } - - if flow.is_some() { - vm.flow = flow; - } - - Ok(output) - } -} - -/// Whether the expression always evaluates to the same value. -fn is_invariant(expr: &SyntaxNode) -> bool { - match expr.cast() { - Some(ast::Expr::Ident(_)) => false, - Some(ast::Expr::MathIdent(_)) => false, - Some(ast::Expr::FieldAccess(access)) => { - is_invariant(access.target().as_untyped()) - } - Some(ast::Expr::FuncCall(call)) => { - is_invariant(call.callee().as_untyped()) - && is_invariant(call.args().as_untyped()) - } - _ => expr.children().all(is_invariant), - } -} - -/// Whether the expression contains a break or return. -fn can_diverge(expr: &SyntaxNode) -> bool { - matches!(expr.kind(), SyntaxKind::Break | SyntaxKind::Return) - || expr.children().any(can_diverge) -} - -impl Eval for ast::ForLoop { - type Output = Value; - - #[tracing::instrument(name = "ForLoop::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let flow = vm.flow.take(); - let mut output = Value::None; - - macro_rules! iter { - (for $pat:ident in $iter:expr) => {{ - vm.scopes.enter(); - - #[allow(unused_parens)] - for value in $iter { - $pat.define(vm, value.into_value())?; - - let body = self.body(); - let value = body.eval(vm)?; - output = ops::join(output, value).at(body.span())?; - - match vm.flow { - Some(FlowEvent::Break(_)) => { - vm.flow = None; - break; - } - Some(FlowEvent::Continue(_)) => vm.flow = None, - Some(FlowEvent::Return(..)) => break, - None => {} - } - } - - vm.scopes.exit(); - }}; - } - - let iter = self.iter().eval(vm)?; - let pattern = self.pattern(); - - match (&pattern, iter.clone()) { - (ast::Pattern::Normal(_), Value::Str(string)) => { - // Iterate over graphemes of string. - iter!(for pattern in string.as_str().graphemes(true)); - } - (_, Value::Dict(dict)) => { - // Iterate over pairs of dict. - iter!(for pattern in dict.pairs()); - } - (_, Value::Array(array)) => { - // Iterate over values of array. - iter!(for pattern in array); - } - (ast::Pattern::Normal(_), _) => { - bail!(self.iter().span(), "cannot loop over {}", iter.type_name()); - } - (_, _) => { - bail!(pattern.span(), "cannot destructure values of {}", iter.type_name()) - } - } - - if flow.is_some() { - vm.flow = flow; - } - - Ok(output) - } -} - -/// Applies imports from `import` to the current scope. -fn apply_imports( - imports: Option, - vm: &mut Vm, - source_value: V, - name: impl Fn(&V) -> EcoString, - scope: impl Fn(&V) -> &Scope, -) -> SourceResult<()> { - match imports { - None => { - vm.scopes.top.define(name(&source_value), source_value); - } - Some(ast::Imports::Wildcard) => { - for (var, value) in scope(&source_value).iter() { - vm.scopes.top.define(var.clone(), value.clone()); - } - } - Some(ast::Imports::Items(idents)) => { - let mut errors = vec![]; - let scope = scope(&source_value); - for ident in idents { - if let Some(value) = scope.get(&ident) { - vm.define(ident, value.clone()); - } else { - errors.push(error!(ident.span(), "unresolved import")); - } - } - if !errors.is_empty() { - return Err(Box::new(errors)); - } - } - } - - Ok(()) -} - -impl Eval for ast::ModuleImport { - type Output = Value; - - #[tracing::instrument(name = "ModuleImport::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let span = self.source().span(); - let source = self.source().eval(vm)?; - if let Value::Func(func) = source { - if func.info().is_none() { - bail!(span, "cannot import from user-defined functions"); - } - apply_imports( - self.imports(), - vm, - func, - |func| func.info().unwrap().name.into(), - |func| &func.info().unwrap().scope, - )?; - } else { - let module = import(vm, source, span, true)?; - apply_imports( - self.imports(), - vm, - module, - |module| module.name().clone(), - |module| module.scope(), - )?; - } - - Ok(Value::None) - } -} - -impl Eval for ast::ModuleInclude { - type Output = Content; - - #[tracing::instrument(name = "ModuleInclude::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let span = self.source().span(); - let source = self.source().eval(vm)?; - let module = import(vm, source, span, false)?; - Ok(module.content()) - } -} - -/// Process an import of a module relative to the current location. -fn import( - vm: &mut Vm, - source: Value, - span: Span, - accept_functions: bool, -) -> SourceResult { - let path = match source { - Value::Str(path) => path, - Value::Module(module) => return Ok(module), - v => { - if accept_functions { - bail!(span, "expected path, module or function, found {}", v.type_name()) - } else { - bail!(span, "expected path or module, found {}", v.type_name()) - } - } - }; - - // Handle package and file imports. - let path = path.as_str(); - if path.starts_with('@') { - let spec = path.parse::().at(span)?; - import_package(vm, spec, span) - } else { - import_file(vm, path, span) - } -} - -/// Import an external package. -fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult { - // Evaluate the manifest. - let manifest_id = FileId::new(Some(spec.clone()), Path::new("/typst.toml")); - let bytes = vm.world().file(manifest_id).at(span)?; - let manifest = PackageManifest::parse(&bytes).at(span)?; - manifest.validate(&spec).at(span)?; - - // Evaluate the entry point. - let entrypoint_id = manifest_id.join(&manifest.package.entrypoint).at(span)?; - let source = vm.world().source(entrypoint_id).at(span)?; - let point = || Tracepoint::Import; - Ok(eval(vm.world(), vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) - .trace(vm.world(), point, span)? - .with_name(manifest.package.name)) -} - -/// Import a file from a path. -fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult { - // Load the source file. - let world = vm.world(); - let id = vm.location().join(path).at(span)?; - let source = world.source(id).at(span)?; - - // Prevent cyclic importing. - if vm.route.contains(source.id()) { - bail!(span, "cyclic import"); - } - - // Evaluate the file. - let point = || Tracepoint::Import; - eval(world, vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) - .trace(world, point, span) -} - -impl Eval for ast::LoopBreak { - type Output = Value; - - #[tracing::instrument(name = "LoopBreak::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - if vm.flow.is_none() { - vm.flow = Some(FlowEvent::Break(self.span())); - } - Ok(Value::None) - } -} - -impl Eval for ast::LoopContinue { - type Output = Value; - - #[tracing::instrument(name = "LoopContinue::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - if vm.flow.is_none() { - vm.flow = Some(FlowEvent::Continue(self.span())); - } - Ok(Value::None) - } -} - -impl Eval for ast::FuncReturn { - type Output = Value; - - #[tracing::instrument(name = "FuncReturn::eval", skip_all)] - fn eval(&self, vm: &mut Vm) -> SourceResult { - let value = self.body().map(|body| body.eval(vm)).transpose()?; - if vm.flow.is_none() { - vm.flow = Some(FlowEvent::Return(self.span(), value)); - } - Ok(Value::None) - } -} - -/// Access an expression mutably. -trait Access { - /// Access the value. - fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value>; -} - -impl Access for ast::Expr { - fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - match self { - Self::Ident(v) => v.access(vm), - Self::Parenthesized(v) => v.access(vm), - Self::FieldAccess(v) => v.access(vm), - Self::FuncCall(v) => v.access(vm), - _ => { - let _ = self.eval(vm)?; - bail!(self.span(), "cannot mutate a temporary value"); - } - } - } -} - -impl Access for ast::Ident { - fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - let span = self.span(); - let value = vm.scopes.get_mut(self).at(span)?; - if vm.traced == Some(span) { - vm.vt.tracer.trace(value.clone()); - } - Ok(value) - } -} - -impl Access for ast::Parenthesized { - fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - self.expr().access(vm) - } -} - -impl Access for ast::FieldAccess { - fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - self.access_dict(vm)?.at_mut(&self.field().take()).at(self.span()) - } -} - -impl ast::FieldAccess { - fn access_dict<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Dict> { - match self.target().access(vm)? { - Value::Dict(dict) => Ok(dict), - value => bail!( - self.target().span(), - "expected dictionary, found {}", - value.type_name(), - ), - } - } -} - -impl Access for ast::FuncCall { - fn access<'a>(&self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - if let ast::Expr::FieldAccess(access) = self.callee() { - let method = access.field().take(); - if methods::is_accessor(&method) { - let span = self.span(); - let world = vm.world(); - let args = self.args().eval(vm)?; - let value = access.target().access(vm)?; - let result = methods::call_access(value, &method, args, span); - let point = || Tracepoint::Call(Some(method.clone())); - return result.trace(world, point, span); - } - } - - let _ = self.eval(vm)?; - bail!(self.span(), "cannot mutate a temporary value"); - } -} diff --git a/src/eval/module.rs b/src/eval/module.rs deleted file mode 100644 index 0bc6bf38..00000000 --- a/src/eval/module.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::sync::Arc; - -use ecow::{eco_format, EcoString}; - -use super::{Content, Scope, Value}; -use crate::diag::StrResult; - -/// An evaluated module, ready for importing or typesetting. -/// -/// Values of this type are cheap to clone and hash. -#[derive(Clone, Hash)] -#[allow(clippy::derived_hash_with_manual_eq)] -pub struct Module { - /// The module's name. - name: EcoString, - /// The reference-counted inner fields. - inner: Arc, -} - -/// The internal representation. -#[derive(Clone, Hash)] -struct Repr { - /// The top-level definitions that were bound in this module. - scope: Scope, - /// The module's layoutable contents. - content: Content, -} - -impl Module { - /// Create a new module. - pub fn new(name: impl Into) -> Self { - Self { - name: name.into(), - inner: Arc::new(Repr { scope: Scope::new(), content: Content::empty() }), - } - } - - /// Update the module's name. - pub fn with_name(mut self, name: impl Into) -> Self { - self.name = name.into(); - self - } - - /// Update the module's scope. - pub fn with_scope(mut self, scope: Scope) -> Self { - Arc::make_mut(&mut self.inner).scope = scope; - self - } - - /// Update the module's content. - pub fn with_content(mut self, content: Content) -> Self { - Arc::make_mut(&mut self.inner).content = content; - self - } - - /// Get the module's name. - pub fn name(&self) -> &EcoString { - &self.name - } - - /// Access the module's scope. - pub fn scope(&self) -> &Scope { - &self.inner.scope - } - - /// Access the module's scope, mutably. - pub fn scope_mut(&mut self) -> &mut Scope { - &mut Arc::make_mut(&mut self.inner).scope - } - - /// Try to access a definition in the module. - pub fn get(&self, name: &str) -> StrResult<&Value> { - self.scope().get(name).ok_or_else(|| { - eco_format!("module `{}` does not contain `{name}`", self.name()) - }) - } - - /// Extract the module's content. - pub fn content(self) -> Content { - match Arc::try_unwrap(self.inner) { - Ok(repr) => repr.content, - Err(arc) => arc.content.clone(), - } - } -} - -impl Debug for Module { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "", self.name()) - } -} - -impl PartialEq for Module { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && Arc::ptr_eq(&self.inner, &other.inner) - } -} diff --git a/src/eval/none.rs b/src/eval/none.rs deleted file mode 100644 index ab7644a7..00000000 --- a/src/eval/none.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use super::{cast, CastInfo, FromValue, IntoValue, Reflect, Value}; -use crate::diag::StrResult; - -/// A value that indicates the absence of any other value. -#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct NoneValue; - -impl Reflect for NoneValue { - fn describe() -> CastInfo { - CastInfo::Type("none") - } - - fn castable(value: &Value) -> bool { - matches!(value, Value::None) - } -} - -impl IntoValue for NoneValue { - fn into_value(self) -> Value { - Value::None - } -} - -impl FromValue for NoneValue { - fn from_value(value: Value) -> StrResult { - match value { - Value::None => Ok(Self), - _ => Err(Self::error(&value)), - } - } -} - -impl Debug for NoneValue { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("none") - } -} - -cast! { - (), - self => Value::None, - _: NoneValue => (), -} - -impl Reflect for Option { - fn describe() -> CastInfo { - T::describe() + NoneValue::describe() - } - - fn castable(value: &Value) -> bool { - NoneValue::castable(value) || T::castable(value) - } -} - -impl IntoValue for Option { - fn into_value(self) -> Value { - match self { - Some(v) => v.into_value(), - None => Value::None, - } - } -} - -impl FromValue for Option { - fn from_value(value: Value) -> StrResult { - match value { - Value::None => Ok(None), - v if T::castable(&v) => Ok(Some(T::from_value(v)?)), - _ => Err(Self::error(&value)), - } - } -} diff --git a/src/eval/ops.rs b/src/eval/ops.rs deleted file mode 100644 index 0880a87e..00000000 --- a/src/eval/ops.rs +++ /dev/null @@ -1,429 +0,0 @@ -//! Operations on values. - -use std::cmp::Ordering; -use std::fmt::Debug; - -use ecow::eco_format; - -use super::{format_str, Regex, Value}; -use crate::diag::{bail, StrResult}; -use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart}; -use Value::*; - -/// Bail with a type mismatch error. -macro_rules! mismatch { - ($fmt:expr, $($value:expr),* $(,)?) => { - return Err(eco_format!($fmt, $($value.type_name()),*)) - }; -} - -/// Join a value with another value. -pub fn join(lhs: Value, rhs: Value) -> StrResult { - Ok(match (lhs, rhs) { - (a, None) => a, - (None, b) => b, - (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")), - (Str(a), Str(b)) => Str(a + b), - (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")), - (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), - (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())), - (Content(a), Str(b)) => Content(a + item!(text)(b.into())), - (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), - (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b), - (Array(a), Array(b)) => Array(a + b), - (Dict(a), Dict(b)) => Dict(a + b), - (a, b) => mismatch!("cannot join {} with {}", a, b), - }) -} - -/// Apply the unary plus operator to a value. -pub fn pos(value: Value) -> StrResult { - Ok(match value { - Int(v) => Int(v), - Float(v) => Float(v), - Length(v) => Length(v), - Angle(v) => Angle(v), - Ratio(v) => Ratio(v), - Relative(v) => Relative(v), - Fraction(v) => Fraction(v), - v => mismatch!("cannot apply '+' to {}", v), - }) -} - -/// Compute the negation of a value. -pub fn neg(value: Value) -> StrResult { - Ok(match value { - Int(v) => Int(v.checked_neg().ok_or("value is too large")?), - Float(v) => Float(-v), - Length(v) => Length(-v), - Angle(v) => Angle(-v), - Ratio(v) => Ratio(-v), - Relative(v) => Relative(-v), - Fraction(v) => Fraction(-v), - v => mismatch!("cannot apply '-' to {}", v), - }) -} - -/// Compute the sum of two values. -pub fn add(lhs: Value, rhs: Value) -> StrResult { - Ok(match (lhs, rhs) { - (a, None) => a, - (None, b) => b, - - (Int(a), Int(b)) => Int(a.checked_add(b).ok_or("value is too large")?), - (Int(a), Float(b)) => Float(a as f64 + b), - (Float(a), Int(b)) => Float(a + b as f64), - (Float(a), Float(b)) => Float(a + b), - - (Angle(a), Angle(b)) => Angle(a + b), - - (Length(a), Length(b)) => Length(a + b), - (Length(a), Ratio(b)) => Relative(b + a), - (Length(a), Relative(b)) => Relative(b + a), - - (Ratio(a), Length(b)) => Relative(a + b), - (Ratio(a), Ratio(b)) => Ratio(a + b), - (Ratio(a), Relative(b)) => Relative(b + a), - - (Relative(a), Length(b)) => Relative(a + b), - (Relative(a), Ratio(b)) => Relative(a + b), - (Relative(a), Relative(b)) => Relative(a + b), - - (Fraction(a), Fraction(b)) => Fraction(a + b), - - (Symbol(a), Symbol(b)) => Str(format_str!("{a}{b}")), - (Str(a), Str(b)) => Str(a + b), - (Str(a), Symbol(b)) => Str(format_str!("{a}{b}")), - (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), - (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())), - (Content(a), Str(b)) => Content(a + item!(text)(b.into())), - (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), - (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b), - - (Array(a), Array(b)) => Array(a + b), - (Dict(a), Dict(b)) => Dict(a + b), - - (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { - Value::dynamic(PartialStroke { - paint: Smart::Custom(color.into()), - thickness: Smart::Custom(thickness), - ..PartialStroke::default() - }) - } - - (Dyn(a), Dyn(b)) => { - // 1D alignments can be summed into 2D alignments. - if let (Some(&a), Some(&b)) = - (a.downcast::(), b.downcast::()) - { - if a.axis() == b.axis() { - return Err(eco_format!("cannot add two {:?} alignments", a.axis())); - } - - return Ok(Value::dynamic(match a.axis() { - Axis::X => Axes { x: a, y: b }, - Axis::Y => Axes { x: b, y: a }, - })); - }; - - mismatch!("cannot add {} and {}", a, b); - } - - (a, b) => mismatch!("cannot add {} and {}", a, b), - }) -} - -/// Compute the difference of two values. -pub fn sub(lhs: Value, rhs: Value) -> StrResult { - Ok(match (lhs, rhs) { - (Int(a), Int(b)) => Int(a.checked_sub(b).ok_or("value is too large")?), - (Int(a), Float(b)) => Float(a as f64 - b), - (Float(a), Int(b)) => Float(a - b as f64), - (Float(a), Float(b)) => Float(a - b), - - (Angle(a), Angle(b)) => Angle(a - b), - - (Length(a), Length(b)) => Length(a - b), - (Length(a), Ratio(b)) => Relative(-b + a), - (Length(a), Relative(b)) => Relative(-b + a), - - (Ratio(a), Length(b)) => Relative(a + -b), - (Ratio(a), Ratio(b)) => Ratio(a - b), - (Ratio(a), Relative(b)) => Relative(-b + a), - - (Relative(a), Length(b)) => Relative(a + -b), - (Relative(a), Ratio(b)) => Relative(a + -b), - (Relative(a), Relative(b)) => Relative(a - b), - - (Fraction(a), Fraction(b)) => Fraction(a - b), - - (a, b) => mismatch!("cannot subtract {1} from {0}", a, b), - }) -} - -/// Compute the product of two values. -pub fn mul(lhs: Value, rhs: Value) -> StrResult { - Ok(match (lhs, rhs) { - (Int(a), Int(b)) => Int(a.checked_mul(b).ok_or("value is too large")?), - (Int(a), Float(b)) => Float(a as f64 * b), - (Float(a), Int(b)) => Float(a * b as f64), - (Float(a), Float(b)) => Float(a * b), - - (Length(a), Int(b)) => Length(a * b as f64), - (Length(a), Float(b)) => Length(a * b), - (Length(a), Ratio(b)) => Length(a * b.get()), - (Int(a), Length(b)) => Length(b * a as f64), - (Float(a), Length(b)) => Length(b * a), - (Ratio(a), Length(b)) => Length(b * a.get()), - - (Angle(a), Int(b)) => Angle(a * b as f64), - (Angle(a), Float(b)) => Angle(a * b), - (Angle(a), Ratio(b)) => Angle(a * b.get()), - (Int(a), Angle(b)) => Angle(a as f64 * b), - (Float(a), Angle(b)) => Angle(a * b), - (Ratio(a), Angle(b)) => Angle(a.get() * b), - - (Ratio(a), Ratio(b)) => Ratio(a * b), - (Ratio(a), Int(b)) => Ratio(a * b as f64), - (Ratio(a), Float(b)) => Ratio(a * b), - (Int(a), Ratio(b)) => Ratio(a as f64 * b), - (Float(a), Ratio(b)) => Ratio(a * b), - - (Relative(a), Int(b)) => Relative(a * b as f64), - (Relative(a), Float(b)) => Relative(a * b), - (Relative(a), Ratio(b)) => Relative(a * b.get()), - (Int(a), Relative(b)) => Relative(a as f64 * b), - (Float(a), Relative(b)) => Relative(a * b), - (Ratio(a), Relative(b)) => Relative(a.get() * b), - - (Fraction(a), Int(b)) => Fraction(a * b as f64), - (Fraction(a), Float(b)) => Fraction(a * b), - (Fraction(a), Ratio(b)) => Fraction(a * b.get()), - (Int(a), Fraction(b)) => Fraction(a as f64 * b), - (Float(a), Fraction(b)) => Fraction(a * b), - (Ratio(a), Fraction(b)) => Fraction(a.get() * b), - - (Str(a), Int(b)) => Str(a.repeat(b)?), - (Int(a), Str(b)) => Str(b.repeat(a)?), - (Array(a), Int(b)) => Array(a.repeat(b)?), - (Int(a), Array(b)) => Array(b.repeat(a)?), - (Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)), - (a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)), - - (a, b) => mismatch!("cannot multiply {} with {}", a, b), - }) -} - -/// Compute the quotient of two values. -pub fn div(lhs: Value, rhs: Value) -> StrResult { - if is_zero(&rhs) { - bail!("cannot divide by zero"); - } - - Ok(match (lhs, rhs) { - (Int(a), Int(b)) => Float(a as f64 / b as f64), - (Int(a), Float(b)) => Float(a as f64 / b), - (Float(a), Int(b)) => Float(a / b as f64), - (Float(a), Float(b)) => Float(a / b), - - (Length(a), Int(b)) => Length(a / b as f64), - (Length(a), Float(b)) => Length(a / b), - (Length(a), Length(b)) => Float(try_div_length(a, b)?), - (Length(a), Relative(b)) if b.rel.is_zero() => Float(try_div_length(a, b.abs)?), - - (Angle(a), Int(b)) => Angle(a / b as f64), - (Angle(a), Float(b)) => Angle(a / b), - (Angle(a), Angle(b)) => Float(a / b), - - (Ratio(a), Int(b)) => Ratio(a / b as f64), - (Ratio(a), Float(b)) => Ratio(a / b), - (Ratio(a), Ratio(b)) => Float(a / b), - (Ratio(a), Relative(b)) if b.abs.is_zero() => Float(a / b.rel), - - (Relative(a), Int(b)) => Relative(a / b as f64), - (Relative(a), Float(b)) => Relative(a / b), - (Relative(a), Length(b)) if a.rel.is_zero() => Float(try_div_length(a.abs, b)?), - (Relative(a), Ratio(b)) if a.abs.is_zero() => Float(a.rel / b), - (Relative(a), Relative(b)) => Float(try_div_relative(a, b)?), - - (Fraction(a), Int(b)) => Fraction(a / b as f64), - (Fraction(a), Float(b)) => Fraction(a / b), - (Fraction(a), Fraction(b)) => Float(a / b), - - (a, b) => mismatch!("cannot divide {} by {}", a, b), - }) -} - -/// Whether a value is a numeric zero. -fn is_zero(v: &Value) -> bool { - match *v { - Int(v) => v == 0, - Float(v) => v == 0.0, - Length(v) => v.is_zero(), - Angle(v) => v.is_zero(), - Ratio(v) => v.is_zero(), - Relative(v) => v.is_zero(), - Fraction(v) => v.is_zero(), - _ => false, - } -} - -/// Try to divide two lengths. -fn try_div_length(a: Length, b: Length) -> StrResult { - a.try_div(b).ok_or_else(|| "cannot divide these two lengths".into()) -} - -/// Try to divide two relative lengths. -fn try_div_relative(a: Rel, b: Rel) -> StrResult { - a.try_div(b) - .ok_or_else(|| "cannot divide these two relative lengths".into()) -} - -/// Compute the logical "not" of a value. -pub fn not(value: Value) -> StrResult { - match value { - Bool(b) => Ok(Bool(!b)), - v => mismatch!("cannot apply 'not' to {}", v), - } -} - -/// Compute the logical "and" of two values. -pub fn and(lhs: Value, rhs: Value) -> StrResult { - match (lhs, rhs) { - (Bool(a), Bool(b)) => Ok(Bool(a && b)), - (a, b) => mismatch!("cannot apply 'and' to {} and {}", a, b), - } -} - -/// Compute the logical "or" of two values. -pub fn or(lhs: Value, rhs: Value) -> StrResult { - match (lhs, rhs) { - (Bool(a), Bool(b)) => Ok(Bool(a || b)), - (a, b) => mismatch!("cannot apply 'or' to {} and {}", a, b), - } -} - -/// Compute whether two values are equal. -pub fn eq(lhs: Value, rhs: Value) -> StrResult { - Ok(Bool(equal(&lhs, &rhs))) -} - -/// Compute whether two values are unequal. -pub fn neq(lhs: Value, rhs: Value) -> StrResult { - Ok(Bool(!equal(&lhs, &rhs))) -} - -macro_rules! comparison { - ($name:ident, $op:tt, $($pat:tt)*) => { - /// Compute how a value compares with another value. - pub fn $name(lhs: Value, rhs: Value) -> StrResult { - let ordering = compare(&lhs, &rhs)?; - Ok(Bool(matches!(ordering, $($pat)*))) - } - }; -} - -comparison!(lt, "<", Ordering::Less); -comparison!(leq, "<=", Ordering::Less | Ordering::Equal); -comparison!(gt, ">", Ordering::Greater); -comparison!(geq, ">=", Ordering::Greater | Ordering::Equal); - -/// Determine whether two values are equal. -pub fn equal(lhs: &Value, rhs: &Value) -> bool { - match (lhs, rhs) { - // Compare reflexively. - (None, None) => true, - (Auto, Auto) => true, - (Bool(a), Bool(b)) => a == b, - (Int(a), Int(b)) => a == b, - (Float(a), Float(b)) => a == b, - (Length(a), Length(b)) => a == b, - (Angle(a), Angle(b)) => a == b, - (Ratio(a), Ratio(b)) => a == b, - (Relative(a), Relative(b)) => a == b, - (Fraction(a), Fraction(b)) => a == b, - (Color(a), Color(b)) => a == b, - (Symbol(a), Symbol(b)) => a == b, - (Str(a), Str(b)) => a == b, - (Label(a), Label(b)) => a == b, - (Content(a), Content(b)) => a == b, - (Array(a), Array(b)) => a == b, - (Dict(a), Dict(b)) => a == b, - (Func(a), Func(b)) => a == b, - (Args(a), Args(b)) => a == b, - (Module(a), Module(b)) => a == b, - (Dyn(a), Dyn(b)) => a == b, - - // Some technically different things should compare equal. - (&Int(a), &Float(b)) => a as f64 == b, - (&Float(a), &Int(b)) => a == b as f64, - (&Length(a), &Relative(b)) => a == b.abs && b.rel.is_zero(), - (&Ratio(a), &Relative(b)) => a == b.rel && b.abs.is_zero(), - (&Relative(a), &Length(b)) => a.abs == b && a.rel.is_zero(), - (&Relative(a), &Ratio(b)) => a.rel == b && a.abs.is_zero(), - - _ => false, - } -} - -/// Compare two values. -pub fn compare(lhs: &Value, rhs: &Value) -> StrResult { - Ok(match (lhs, rhs) { - (Bool(a), Bool(b)) => a.cmp(b), - (Int(a), Int(b)) => a.cmp(b), - (Float(a), Float(b)) => try_cmp_values(a, b)?, - (Length(a), Length(b)) => try_cmp_values(a, b)?, - (Angle(a), Angle(b)) => a.cmp(b), - (Ratio(a), Ratio(b)) => a.cmp(b), - (Relative(a), Relative(b)) => try_cmp_values(a, b)?, - (Fraction(a), Fraction(b)) => a.cmp(b), - (Str(a), Str(b)) => a.cmp(b), - - // Some technically different things should be comparable. - (Int(a), Float(b)) => try_cmp_values(&(*a as f64), b)?, - (Float(a), Int(b)) => try_cmp_values(a, &(*b as f64))?, - (Length(a), Relative(b)) if b.rel.is_zero() => try_cmp_values(a, &b.abs)?, - (Ratio(a), Relative(b)) if b.abs.is_zero() => a.cmp(&b.rel), - (Relative(a), Length(b)) if a.rel.is_zero() => try_cmp_values(&a.abs, b)?, - (Relative(a), Ratio(b)) if a.abs.is_zero() => a.rel.cmp(b), - - _ => mismatch!("cannot compare {} and {}", lhs, rhs), - }) -} - -/// Try to compare two values. -fn try_cmp_values(a: &T, b: &T) -> StrResult { - a.partial_cmp(b) - .ok_or_else(|| eco_format!("cannot compare {:?} with {:?}", a, b)) -} - -/// Test whether one value is "in" another one. -pub fn in_(lhs: Value, rhs: Value) -> StrResult { - if let Some(b) = contains(&lhs, &rhs) { - Ok(Bool(b)) - } else { - mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) - } -} - -/// Test whether one value is "not in" another one. -pub fn not_in(lhs: Value, rhs: Value) -> StrResult { - if let Some(b) = contains(&lhs, &rhs) { - Ok(Bool(!b)) - } else { - mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) - } -} - -/// Test for containment. -pub fn contains(lhs: &Value, rhs: &Value) -> Option { - match (lhs, rhs) { - (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), - (Dyn(a), Str(b)) => a.downcast::().map(|regex| regex.is_match(b)), - (Str(a), Dict(b)) => Some(b.contains(a)), - (a, Array(b)) => Some(b.contains(a)), - _ => Option::None, - } -} diff --git a/src/eval/scope.rs b/src/eval/scope.rs deleted file mode 100644 index f3e13715..00000000 --- a/src/eval/scope.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::collections::BTreeMap; -use std::fmt::{self, Debug, Formatter}; -use std::hash::Hash; - -use ecow::{eco_format, EcoString}; - -use super::{IntoValue, Library, Value}; -use crate::diag::{bail, StrResult}; - -/// A stack of scopes. -#[derive(Debug, Default, Clone)] -pub struct Scopes<'a> { - /// The active scope. - pub top: Scope, - /// The stack of lower scopes. - pub scopes: Vec, - /// The standard library. - pub base: Option<&'a Library>, -} - -impl<'a> Scopes<'a> { - /// Create a new, empty hierarchy of scopes. - pub fn new(base: Option<&'a Library>) -> Self { - Self { top: Scope::new(), scopes: vec![], base } - } - - /// Enter a new scope. - pub fn enter(&mut self) { - self.scopes.push(std::mem::take(&mut self.top)); - } - - /// Exit the topmost scope. - /// - /// This panics if no scope was entered. - pub fn exit(&mut self) { - self.top = self.scopes.pop().expect("no pushed scope"); - } - - /// Try to access a variable immutably. - pub fn get(&self, var: &str) -> StrResult<&Value> { - std::iter::once(&self.top) - .chain(self.scopes.iter().rev()) - .chain(self.base.map(|base| base.global.scope())) - .find_map(|scope| scope.get(var)) - .ok_or_else(|| unknown_variable(var)) - } - - /// Try to access a variable immutably in math. - pub fn get_in_math(&self, var: &str) -> StrResult<&Value> { - std::iter::once(&self.top) - .chain(self.scopes.iter().rev()) - .chain(self.base.map(|base| base.math.scope())) - .find_map(|scope| scope.get(var)) - .ok_or_else(|| eco_format!("unknown variable: {}", var)) - } - - /// Try to access a variable mutably. - pub fn get_mut(&mut self, var: &str) -> StrResult<&mut Value> { - std::iter::once(&mut self.top) - .chain(&mut self.scopes.iter_mut().rev()) - .find_map(|scope| scope.get_mut(var)) - .ok_or_else(|| { - match self.base.and_then(|base| base.global.scope().get(var)) { - Some(_) => eco_format!("cannot mutate a constant: {}", var), - _ => unknown_variable(var), - } - })? - } -} - -/// The error message when a variable is not found. -#[cold] -fn unknown_variable(var: &str) -> EcoString { - if var.contains('-') { - eco_format!("unknown variable: {} - if you meant to use subtraction, try adding spaces around the minus sign.", var) - } else { - eco_format!("unknown variable: {}", var) - } -} - -/// A map from binding names to values. -#[derive(Default, Clone, Hash)] -pub struct Scope(BTreeMap, bool); - -impl Scope { - /// Create a new empty scope. - pub fn new() -> Self { - Self(BTreeMap::new(), false) - } - - /// Create a new scope with duplication prevention. - pub fn deduplicating() -> Self { - Self(BTreeMap::new(), true) - } - - /// Bind a value to a name. - #[track_caller] - pub fn define(&mut self, name: impl Into, value: impl IntoValue) { - let name = name.into(); - - #[cfg(debug_assertions)] - if self.1 && self.0.contains_key(&name) { - panic!("duplicate definition: {name}"); - } - - self.0.insert(name, Slot::new(value.into_value(), Kind::Normal)); - } - - /// Define a captured, immutable binding. - pub fn define_captured(&mut self, var: impl Into, value: impl IntoValue) { - self.0 - .insert(var.into(), Slot::new(value.into_value(), Kind::Captured)); - } - - /// Try to access a variable immutably. - pub fn get(&self, var: &str) -> Option<&Value> { - self.0.get(var).map(Slot::read) - } - - /// Try to access a variable mutably. - pub fn get_mut(&mut self, var: &str) -> Option> { - self.0.get_mut(var).map(Slot::write) - } - - /// Iterate over all definitions. - pub fn iter(&self) -> impl Iterator { - self.0.iter().map(|(k, v)| (k, v.read())) - } -} - -impl Debug for Scope { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Scope ")?; - f.debug_map() - .entries(self.0.iter().map(|(k, v)| (k, v.read()))) - .finish() - } -} - -/// A slot where a value is stored. -#[derive(Clone, Hash)] -struct Slot { - /// The stored value. - value: Value, - /// The kind of slot, determines how the value can be accessed. - kind: Kind, -} - -/// The different kinds of slots. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum Kind { - /// A normal, mutable binding. - Normal, - /// A captured copy of another variable. - Captured, -} - -impl Slot { - /// Create a new slot. - fn new(value: Value, kind: Kind) -> Self { - Self { value, kind } - } - - /// Read the value. - fn read(&self) -> &Value { - &self.value - } - - /// Try to write to the value. - fn write(&mut self) -> StrResult<&mut Value> { - match self.kind { - Kind::Normal => Ok(&mut self.value), - Kind::Captured => { - bail!("variables from outside the function are read-only and cannot be modified") - } - } - } -} diff --git a/src/eval/str.rs b/src/eval/str.rs deleted file mode 100644 index f5e5ab00..00000000 --- a/src/eval/str.rs +++ /dev/null @@ -1,620 +0,0 @@ -use std::borrow::{Borrow, Cow}; -use std::fmt::{self, Debug, Display, Formatter, Write}; -use std::hash::{Hash, Hasher}; -use std::ops::{Add, AddAssign, Deref, Range}; - -use ecow::EcoString; -use unicode_segmentation::UnicodeSegmentation; - -use super::{cast, dict, Args, Array, Dict, Func, IntoValue, Value, Vm}; -use crate::diag::{bail, At, SourceResult, StrResult}; -use crate::geom::GenAlign; - -/// Create a new [`Str`] from a format string. -#[macro_export] -#[doc(hidden)] -macro_rules! __format_str { - ($($tts:tt)*) => {{ - $crate::eval::Str::from($crate::eval::eco_format!($($tts)*)) - }}; -} - -#[doc(inline)] -pub use crate::__format_str as format_str; -#[doc(hidden)] -pub use ecow::eco_format; - -/// An immutable reference counted string. -#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Str(EcoString); - -impl Str { - /// Create a new, empty string. - pub fn new() -> Self { - Self(EcoString::new()) - } - - /// Return `true` if the length is 0. - pub fn is_empty(&self) -> bool { - self.0.len() == 0 - } - - /// The length of the string in bytes. - pub fn len(&self) -> usize { - self.0.len() - } - - /// A string slice containing the entire string. - pub fn as_str(&self) -> &str { - self - } - - /// Extract the first grapheme cluster. - pub fn first(&self) -> StrResult { - self.0 - .graphemes(true) - .next() - .map(Into::into) - .ok_or_else(string_is_empty) - } - - /// Extract the last grapheme cluster. - pub fn last(&self) -> StrResult { - self.0 - .graphemes(true) - .next_back() - .map(Into::into) - .ok_or_else(string_is_empty) - } - - /// Extract the grapheme cluster at the given index. - pub fn at<'a>(&'a self, index: i64, default: Option<&'a str>) -> StrResult { - let len = self.len(); - let grapheme = self - .locate_opt(index)? - .and_then(|i| self.0[i..].graphemes(true).next()) - .or(default) - .ok_or_else(|| no_default_and_out_of_bounds(index, len))?; - Ok(grapheme.into()) - } - - /// Extract a contiguous substring. - 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()) - } - - /// The grapheme clusters the string consists of. - pub fn clusters(&self) -> Array { - self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect() - } - - /// The codepoints the string consists of. - pub fn codepoints(&self) -> Array { - self.chars().map(|c| Value::Str(c.into())).collect() - } - - /// Whether the given pattern exists in this string. - pub fn contains(&self, pattern: StrPattern) -> bool { - match pattern { - StrPattern::Str(pat) => self.0.contains(pat.as_str()), - StrPattern::Regex(re) => re.is_match(self), - } - } - - /// Whether this string begins with the given pattern. - pub fn starts_with(&self, pattern: StrPattern) -> bool { - match pattern { - StrPattern::Str(pat) => self.0.starts_with(pat.as_str()), - StrPattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0), - } - } - - /// Whether this string ends with the given pattern. - pub fn ends_with(&self, pattern: StrPattern) -> bool { - match pattern { - StrPattern::Str(pat) => self.0.ends_with(pat.as_str()), - StrPattern::Regex(re) => { - re.find_iter(self).last().map_or(false, |m| m.end() == self.0.len()) - } - } - } - - /// The text of the pattern's first match in this string. - pub fn find(&self, pattern: StrPattern) -> Option { - match pattern { - StrPattern::Str(pat) => self.0.contains(pat.as_str()).then_some(pat), - StrPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()), - } - } - - /// The position of the pattern's first match in this string. - pub fn position(&self, pattern: StrPattern) -> Option { - match pattern { - StrPattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64), - StrPattern::Regex(re) => re.find(self).map(|m| m.start() as i64), - } - } - - /// The start and, text and capture groups (if any) of the first match of - /// the pattern in this string. - pub fn match_(&self, pattern: StrPattern) -> Option { - match pattern { - StrPattern::Str(pat) => { - self.0.match_indices(pat.as_str()).next().map(match_to_dict) - } - StrPattern::Regex(re) => re.captures(self).map(captures_to_dict), - } - } - - /// The start, end, text and capture groups (if any) of all matches of the - /// pattern in this string. - pub fn matches(&self, pattern: StrPattern) -> Array { - match pattern { - StrPattern::Str(pat) => self - .0 - .match_indices(pat.as_str()) - .map(match_to_dict) - .map(Value::Dict) - .collect(), - StrPattern::Regex(re) => re - .captures_iter(self) - .map(captures_to_dict) - .map(Value::Dict) - .collect(), - } - } - - /// Split this string at whitespace or a specific pattern. - pub fn split(&self, pattern: Option) -> Array { - let s = self.as_str(); - match pattern { - None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(), - Some(StrPattern::Str(pat)) => { - s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect() - } - Some(StrPattern::Regex(re)) => { - re.split(s).map(|v| Value::Str(v.into())).collect() - } - } - } - - /// Trim either whitespace or the given pattern at both or just one side of - /// the string. If `repeat` is true, the pattern is trimmed repeatedly - /// instead of just once. Repeat must only be given in combination with a - /// pattern. - pub fn trim( - &self, - pattern: Option, - at: Option, - repeat: bool, - ) -> Self { - let mut start = matches!(at, Some(StrSide::Start) | None); - let end = matches!(at, Some(StrSide::End) | None); - - let trimmed = match pattern { - None => match at { - None => self.0.trim(), - Some(StrSide::Start) => self.0.trim_start(), - Some(StrSide::End) => self.0.trim_end(), - }, - Some(StrPattern::Str(pat)) => { - let pat = pat.as_str(); - let mut s = self.as_str(); - if repeat { - if start { - s = s.trim_start_matches(pat); - } - if end { - s = s.trim_end_matches(pat); - } - } else { - if start { - s = s.strip_prefix(pat).unwrap_or(s); - } - if end { - s = s.strip_suffix(pat).unwrap_or(s); - } - } - s - } - Some(StrPattern::Regex(re)) => { - let s = self.as_str(); - let mut last = 0; - let mut range = 0..s.len(); - - for m in re.find_iter(s) { - // Does this match follow directly after the last one? - let consecutive = last == m.start(); - - // As long as we're consecutive and still trimming at the - // start, trim. - start &= consecutive; - if start { - range.start = m.end(); - start &= repeat; - } - - // Reset end trim if we aren't consecutive anymore or aren't - // repeating. - if end && (!consecutive || !repeat) { - range.end = m.start(); - } - - last = m.end(); - } - - // Is the last match directly at the end? - if last < s.len() { - range.end = s.len(); - } - - &s[range.start..range.start.max(range.end)] - } - }; - - trimmed.into() - } - - /// Replace at most `count` occurrences of the given pattern with a - /// replacement string or function (beginning from the start). If no count - /// is given, all occurrences are replaced. - pub fn replace( - &self, - vm: &mut Vm, - pattern: StrPattern, - with: Replacement, - count: Option, - ) -> SourceResult { - // Heuristic: Assume the new string is about the same length as - // the current string. - let mut output = EcoString::with_capacity(self.as_str().len()); - - // Replace one match of a pattern with the replacement. - let mut last_match = 0; - let mut handle_match = |range: Range, dict: Dict| -> SourceResult<()> { - // Push everything until the match. - output.push_str(&self[last_match..range.start]); - last_match = range.end; - - // Determine and push the replacement. - match &with { - Replacement::Str(s) => output.push_str(s), - Replacement::Func(func) => { - let args = Args::new(func.span(), [dict]); - let piece = func.call_vm(vm, args)?.cast::().at(func.span())?; - output.push_str(&piece); - } - } - - Ok(()) - }; - - // Iterate over the matches of the `pattern`. - let count = count.unwrap_or(usize::MAX); - match &pattern { - StrPattern::Str(pat) => { - for m in self.match_indices(pat.as_str()).take(count) { - let (start, text) = m; - handle_match(start..start + text.len(), match_to_dict(m))?; - } - } - StrPattern::Regex(re) => { - for caps in re.captures_iter(self).take(count) { - // Extract the entire match over all capture groups. - let m = caps.get(0).unwrap(); - handle_match(m.start()..m.end(), captures_to_dict(caps))?; - } - } - } - - // Push the remainder. - output.push_str(&self[last_match..]); - Ok(output.into()) - } - - /// Repeat the string a number of times. - pub fn repeat(&self, n: i64) -> StrResult { - let n = usize::try_from(n) - .ok() - .and_then(|n| self.0.len().checked_mul(n).map(|_| n)) - .ok_or_else(|| format!("cannot repeat this string {} times", n))?; - - Ok(Self(self.0.repeat(n))) - } - - /// Resolve an index, if it is within bounds. - /// Errors on invalid char boundaries. - fn locate_opt(&self, index: i64) -> StrResult> { - let wrapped = - if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; - - let resolved = wrapped - .and_then(|v| usize::try_from(v).ok()) - .filter(|&v| v <= self.0.len()); - - if resolved.map_or(false, |i| !self.0.is_char_boundary(i)) { - return Err(not_a_char_boundary(index)); - } - - Ok(resolved) - } - - /// 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())) - } -} - -/// The out of bounds access error message. -#[cold] -fn out_of_bounds(index: i64, len: usize) -> EcoString { - eco_format!("string index out of bounds (index: {}, len: {})", index, len) -} - -/// The out of bounds access error message when no default value was given. -#[cold] -fn no_default_and_out_of_bounds(index: i64, len: usize) -> EcoString { - eco_format!("no default value was specified and string index out of bounds (index: {}, len: {})", index, len) -} - -/// The char boundary access error message. -#[cold] -fn not_a_char_boundary(index: i64) -> EcoString { - eco_format!("string index {} is not a character boundary", index) -} - -/// The error message when the string is empty. -#[cold] -fn string_is_empty() -> EcoString { - "string is empty".into() -} - -/// Convert an item of std's `match_indices` to a dictionary. -fn match_to_dict((start, text): (usize, &str)) -> Dict { - dict! { - "start" => start, - "end" => start + text.len(), - "text" => text, - "captures" => Array::new(), - } -} - -/// Convert regex captures to a dictionary. -fn captures_to_dict(cap: regex::Captures) -> Dict { - let m = cap.get(0).expect("missing first match"); - dict! { - "start" => m.start(), - "end" => m.end(), - "text" => m.as_str(), - "captures" => cap.iter() - .skip(1) - .map(|opt| opt.map_or(Value::None, |m| m.as_str().into_value())) - .collect::(), - } -} - -impl Deref for Str { - type Target = str; - - fn deref(&self) -> &str { - &self.0 - } -} - -impl Display for Str { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(self) - } -} - -impl Debug for Str { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_char('"')?; - for c in self.chars() { - match c { - '\0' => f.write_str("\\u{0}")?, - '\'' => f.write_str("'")?, - '"' => f.write_str(r#"\""#)?, - _ => Display::fmt(&c.escape_debug(), f)?, - } - } - f.write_char('"') - } -} - -impl Add for Str { - type Output = Self; - - fn add(mut self, rhs: Self) -> Self::Output { - self += rhs; - self - } -} - -impl AddAssign for Str { - fn add_assign(&mut self, rhs: Self) { - self.0.push_str(rhs.as_str()); - } -} - -impl AsRef for Str { - fn as_ref(&self) -> &str { - self - } -} - -impl Borrow for Str { - fn borrow(&self) -> &str { - self - } -} - -impl From for Str { - fn from(c: char) -> Self { - Self(c.into()) - } -} - -impl From<&str> for Str { - fn from(s: &str) -> Self { - Self(s.into()) - } -} - -impl From for Str { - fn from(s: EcoString) -> Self { - Self(s) - } -} - -impl From for Str { - fn from(s: String) -> Self { - Self(s.into()) - } -} - -impl From> for Str { - fn from(s: Cow) -> Self { - Self(s.into()) - } -} - -impl FromIterator for Str { - fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().collect()) - } -} - -impl From for EcoString { - fn from(str: Str) -> Self { - str.0 - } -} - -impl From for String { - fn from(s: Str) -> Self { - s.0.into() - } -} - -cast! { - char, - self => Value::Str(self.into()), - string: Str => { - let mut chars = string.chars(); - match (chars.next(), chars.next()) { - (Some(c), None) => c, - _ => bail!("expected exactly one character"), - } - }, -} - -cast! { - &str, - self => Value::Str(self.into()), -} - -cast! { - EcoString, - self => Value::Str(self.into()), - v: Str => v.into(), -} - -cast! { - String, - self => Value::Str(self.into()), - v: Str => v.into(), -} - -/// A regular expression. -#[derive(Clone)] -pub struct Regex(regex::Regex); - -impl Regex { - /// Create a new regular expression. - pub fn new(re: &str) -> StrResult { - regex::Regex::new(re).map(Self).map_err(|err| eco_format!("{err}")) - } -} - -impl Deref for Regex { - type Target = regex::Regex; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Debug for Regex { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "regex({:?})", self.0.as_str()) - } -} - -impl PartialEq for Regex { - fn eq(&self, other: &Self) -> bool { - self.0.as_str() == other.0.as_str() - } -} - -impl Hash for Regex { - fn hash(&self, state: &mut H) { - self.0.as_str().hash(state); - } -} - -cast! { - type Regex: "regular expression", -} - -/// A pattern which can be searched for in a string. -#[derive(Debug, Clone)] -pub enum StrPattern { - /// Just a string. - Str(Str), - /// A regular expression. - Regex(Regex), -} - -cast! { - StrPattern, - text: Str => Self::Str(text), - regex: Regex => Self::Regex(regex), -} - -/// A side of a string. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum StrSide { - /// The logical start of the string, may be left or right depending on the - /// language. - Start, - /// The logical end of the string. - End, -} - -cast! { - StrSide, - align: GenAlign => match align { - GenAlign::Start => Self::Start, - GenAlign::End => Self::End, - _ => bail!("expected either `start` or `end`"), - }, -} - -/// A replacement for a matched [`Str`] -pub enum Replacement { - /// A string a match is replaced with. - Str(Str), - /// Function of type Dict -> Str (see `captures_to_dict` or `match_to_dict`) - /// whose output is inserted for the match. - Func(Func), -} - -cast! { - Replacement, - text: Str => Self::Str(text), - func: Func => Self::Func(func) -} diff --git a/src/eval/symbol.rs b/src/eval/symbol.rs deleted file mode 100644 index 0925202e..00000000 --- a/src/eval/symbol.rs +++ /dev/null @@ -1,210 +0,0 @@ -use std::cmp::Reverse; -use std::collections::BTreeSet; -use std::fmt::{self, Debug, Display, Formatter, Write}; -use std::sync::Arc; - -use ecow::EcoString; - -use crate::diag::{bail, StrResult}; - -/// A symbol, possibly with variants. -#[derive(Clone, Eq, PartialEq, Hash)] -pub struct Symbol(Repr); - -/// The internal representation. -#[derive(Clone, Eq, PartialEq, Hash)] -enum Repr { - Single(char), - Const(&'static [(&'static str, char)]), - Multi(Arc<(List, EcoString)>), -} - -/// A collection of symbols. -#[derive(Clone, Eq, PartialEq, Hash)] -enum List { - Static(&'static [(&'static str, char)]), - Runtime(Box<[(EcoString, char)]>), -} - -impl Symbol { - /// Create a new symbol from a single character. - pub const fn new(c: char) -> Self { - Self(Repr::Single(c)) - } - - /// Create a symbol with a static variant list. - #[track_caller] - pub const fn list(list: &'static [(&'static str, char)]) -> Self { - debug_assert!(!list.is_empty()); - Self(Repr::Const(list)) - } - - /// Create a symbol with a runtime variant list. - #[track_caller] - pub fn runtime(list: Box<[(EcoString, char)]>) -> Self { - debug_assert!(!list.is_empty()); - Self(Repr::Multi(Arc::new((List::Runtime(list), EcoString::new())))) - } - - /// Get the symbol's text. - pub fn get(&self) -> char { - match &self.0 { - Repr::Single(c) => *c, - Repr::Const(_) => find(self.variants(), "").unwrap(), - Repr::Multi(arc) => find(self.variants(), &arc.1).unwrap(), - } - } - - /// Apply a modifier to the symbol. - pub fn modified(mut self, modifier: &str) -> StrResult { - if let Repr::Const(list) = self.0 { - self.0 = Repr::Multi(Arc::new((List::Static(list), EcoString::new()))); - } - - if let Repr::Multi(arc) = &mut self.0 { - let (list, modifiers) = Arc::make_mut(arc); - if !modifiers.is_empty() { - modifiers.push('.'); - } - modifiers.push_str(modifier); - if find(list.variants(), modifiers).is_some() { - return Ok(self); - } - } - - bail!("unknown symbol modifier") - } - - /// The characters that are covered by this symbol. - pub fn variants(&self) -> impl Iterator { - match &self.0 { - Repr::Single(c) => Variants::Single(Some(*c).into_iter()), - Repr::Const(list) => Variants::Static(list.iter()), - Repr::Multi(arc) => arc.0.variants(), - } - } - - /// Possible modifiers. - pub fn modifiers(&self) -> impl Iterator + '_ { - let mut set = BTreeSet::new(); - let modifiers = match &self.0 { - Repr::Multi(arc) => arc.1.as_str(), - _ => "", - }; - for modifier in self.variants().flat_map(|(name, _)| name.split('.')) { - if !modifier.is_empty() && !contained(modifiers, modifier) { - set.insert(modifier); - } - } - set.into_iter() - } - - /// Normalize an accent to a combining one. - pub fn combining_accent(c: char) -> Option { - Some(match c { - '\u{0300}' | '`' => '\u{0300}', - '\u{0301}' | '´' => '\u{0301}', - '\u{0302}' | '^' | 'ˆ' => '\u{0302}', - '\u{0303}' | '~' | '∼' | '˜' => '\u{0303}', - '\u{0304}' | '¯' => '\u{0304}', - '\u{0305}' | '-' | '‾' | '−' => '\u{0305}', - '\u{0306}' | '˘' => '\u{0306}', - '\u{0307}' | '.' | '˙' | '⋅' => '\u{0307}', - '\u{0308}' | '¨' => '\u{0308}', - '\u{20db}' => '\u{20db}', - '\u{20dc}' => '\u{20dc}', - '\u{030a}' | '∘' | '○' => '\u{030a}', - '\u{030b}' | '˝' => '\u{030b}', - '\u{030c}' | 'ˇ' => '\u{030c}', - '\u{20d6}' | '←' => '\u{20d6}', - '\u{20d7}' | '→' | '⟶' => '\u{20d7}', - _ => return None, - }) - } -} - -impl Debug for Symbol { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_char(self.get()) - } -} - -impl Display for Symbol { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_char(self.get()) - } -} - -impl List { - /// The characters that are covered by this list. - fn variants(&self) -> Variants<'_> { - match self { - List::Static(list) => Variants::Static(list.iter()), - List::Runtime(list) => Variants::Runtime(list.iter()), - } - } -} - -/// Iterator over variants. -enum Variants<'a> { - Single(std::option::IntoIter), - Static(std::slice::Iter<'static, (&'static str, char)>), - Runtime(std::slice::Iter<'a, (EcoString, char)>), -} - -impl<'a> Iterator for Variants<'a> { - type Item = (&'a str, char); - - fn next(&mut self) -> Option { - match self { - Self::Single(iter) => Some(("", iter.next()?)), - Self::Static(list) => list.next().copied(), - Self::Runtime(list) => list.next().map(|(s, c)| (s.as_str(), *c)), - } - } -} - -/// Find the best symbol from the list. -fn find<'a>( - variants: impl Iterator, - modifiers: &str, -) -> Option { - let mut best = None; - let mut best_score = None; - - // Find the best table entry with this name. - 'outer: for candidate in variants { - for modifier in parts(modifiers) { - if !contained(candidate.0, modifier) { - continue 'outer; - } - } - - let mut matching = 0; - let mut total = 0; - for modifier in parts(candidate.0) { - if contained(modifiers, modifier) { - matching += 1; - } - total += 1; - } - - let score = (matching, Reverse(total)); - if best_score.map_or(true, |b| score > b) { - best = Some(candidate.1); - best_score = Some(score); - } - } - - best -} - -/// Split a modifier list into its parts. -fn parts(modifiers: &str) -> impl Iterator { - modifiers.split('.').filter(|s| !s.is_empty()) -} - -/// Whether the modifier string contains the modifier `m`. -fn contained(modifiers: &str, m: &str) -> bool { - parts(modifiers).any(|part| part == m) -} diff --git a/src/eval/value.rs b/src/eval/value.rs deleted file mode 100644 index b1782cab..00000000 --- a/src/eval/value.rs +++ /dev/null @@ -1,461 +0,0 @@ -use std::any::Any; -use std::cmp::Ordering; -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::sync::Arc; - -use ecow::eco_format; -use siphasher::sip128::{Hasher128, SipHasher13}; - -use super::{ - cast, format_str, ops, Args, Array, CastInfo, Content, Dict, FromValue, Func, - IntoValue, Module, Reflect, Str, Symbol, -}; -use crate::diag::StrResult; -use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel}; -use crate::model::{Label, Styles}; -use crate::syntax::{ast, Span}; -use crate::util::Bytes; - -/// A computational value. -#[derive(Default, Clone)] -pub enum Value { - /// The value that indicates the absence of a meaningful value. - #[default] - None, - /// A value that indicates some smart default behaviour. - Auto, - /// A boolean: `true, false`. - Bool(bool), - /// An integer: `120`. - Int(i64), - /// A floating-point number: `1.2`, `10e-4`. - Float(f64), - /// A length: `12pt`, `3cm`, `1.5em`, `1em - 2pt`. - Length(Length), - /// An angle: `1.5rad`, `90deg`. - Angle(Angle), - /// A ratio: `50%`. - Ratio(Ratio), - /// A relative length, combination of a ratio and a length: `20% + 5cm`. - Relative(Rel), - /// A fraction: `1fr`. - Fraction(Fr), - /// A color value: `#f79143ff`. - Color(Color), - /// A symbol: `arrow.l`. - Symbol(Symbol), - /// A string: `"string"`. - Str(Str), - /// Raw bytes. - Bytes(Bytes), - /// A label: ``. - Label(Label), - /// A content value: `[*Hi* there]`. - Content(Content), - // Content styles. - Styles(Styles), - /// An array of values: `(1, "hi", 12cm)`. - Array(Array), - /// A dictionary value: `(a: 1, b: "hi")`. - Dict(Dict), - /// An executable function. - Func(Func), - /// Captured arguments to a function. - Args(Args), - /// A module. - Module(Module), - /// A dynamic value. - Dyn(Dynamic), -} - -impl Value { - /// Create a new dynamic value. - pub fn dynamic(any: T) -> Self - where - T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, - { - Self::Dyn(Dynamic::new(any)) - } - - /// Create a numeric value from a number with a unit. - pub fn numeric(pair: (f64, ast::Unit)) -> Self { - let (v, unit) = pair; - match unit { - ast::Unit::Length(unit) => Abs::with_unit(v, unit).into_value(), - ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into_value(), - ast::Unit::Em => Em::new(v).into_value(), - ast::Unit::Fr => Fr::new(v).into_value(), - ast::Unit::Percent => Ratio::new(v / 100.0).into_value(), - } - } - - /// The name of the stored value's type. - pub fn type_name(&self) -> &'static str { - match self { - Self::None => "none", - Self::Auto => "auto", - Self::Bool(_) => bool::TYPE_NAME, - Self::Int(_) => i64::TYPE_NAME, - Self::Float(_) => f64::TYPE_NAME, - Self::Length(_) => Length::TYPE_NAME, - Self::Angle(_) => Angle::TYPE_NAME, - Self::Ratio(_) => Ratio::TYPE_NAME, - Self::Relative(_) => Rel::::TYPE_NAME, - Self::Fraction(_) => Fr::TYPE_NAME, - Self::Color(_) => Color::TYPE_NAME, - Self::Symbol(_) => Symbol::TYPE_NAME, - Self::Str(_) => Str::TYPE_NAME, - Self::Bytes(_) => Bytes::TYPE_NAME, - Self::Label(_) => Label::TYPE_NAME, - Self::Content(_) => Content::TYPE_NAME, - Self::Styles(_) => Styles::TYPE_NAME, - Self::Array(_) => Array::TYPE_NAME, - Self::Dict(_) => Dict::TYPE_NAME, - Self::Func(_) => Func::TYPE_NAME, - Self::Args(_) => Args::TYPE_NAME, - Self::Module(_) => Module::TYPE_NAME, - Self::Dyn(v) => v.type_name(), - } - } - - /// Try to cast the value into a specific type. - pub fn cast(self) -> StrResult { - T::from_value(self) - } - - /// Try to access a field on the value. - pub fn field(&self, field: &str) -> StrResult { - match self { - Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), - Self::Dict(dict) => dict.at(field, None).cloned(), - Self::Content(content) => content.at(field, None), - Self::Module(module) => module.get(field).cloned(), - Self::Func(func) => func.get(field).cloned(), - v => Err(eco_format!("cannot access fields on type {}", v.type_name())), - } - } - - /// Return the debug representation of the value. - pub fn repr(&self) -> Str { - format_str!("{:?}", self) - } - - /// Attach a span to the value, if possible. - pub fn spanned(self, span: Span) -> Self { - match self { - Value::Content(v) => Value::Content(v.spanned(span)), - Value::Func(v) => Value::Func(v.spanned(span)), - v => v, - } - } - - /// Return the display representation of the value. - pub fn display(self) -> Content { - match self { - Self::None => Content::empty(), - Self::Int(v) => item!(text)(eco_format!("{}", v)), - Self::Float(v) => item!(text)(eco_format!("{}", v)), - Self::Str(v) => item!(text)(v.into()), - Self::Symbol(v) => item!(text)(v.get().into()), - Self::Content(v) => v, - Self::Func(_) => Content::empty(), - Self::Module(module) => module.content(), - _ => item!(raw)(self.repr().into(), Some("typc".into()), false), - } - } - - /// Try to extract documentation for the value. - pub fn docs(&self) -> Option<&'static str> { - match self { - Self::Func(func) => func.info().map(|info| info.docs), - _ => None, - } - } -} - -impl Debug for Value { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::None => f.pad("none"), - Self::Auto => f.pad("auto"), - Self::Bool(v) => Debug::fmt(v, f), - Self::Int(v) => Debug::fmt(v, f), - Self::Float(v) => Debug::fmt(v, f), - Self::Length(v) => Debug::fmt(v, f), - Self::Angle(v) => Debug::fmt(v, f), - Self::Ratio(v) => Debug::fmt(v, f), - Self::Relative(v) => Debug::fmt(v, f), - Self::Fraction(v) => Debug::fmt(v, f), - Self::Color(v) => Debug::fmt(v, f), - Self::Symbol(v) => Debug::fmt(v, f), - Self::Str(v) => Debug::fmt(v, f), - Self::Bytes(v) => Debug::fmt(v, f), - Self::Label(v) => Debug::fmt(v, f), - Self::Content(v) => Debug::fmt(v, f), - Self::Styles(v) => Debug::fmt(v, f), - Self::Array(v) => Debug::fmt(v, f), - Self::Dict(v) => Debug::fmt(v, f), - Self::Func(v) => Debug::fmt(v, f), - Self::Args(v) => Debug::fmt(v, f), - Self::Module(v) => Debug::fmt(v, f), - Self::Dyn(v) => Debug::fmt(v, f), - } - } -} - -impl PartialEq for Value { - fn eq(&self, other: &Self) -> bool { - ops::equal(self, other) - } -} - -impl PartialOrd for Value { - fn partial_cmp(&self, other: &Self) -> Option { - ops::compare(self, other).ok() - } -} - -impl Hash for Value { - fn hash(&self, state: &mut H) { - std::mem::discriminant(self).hash(state); - match self { - Self::None => {} - Self::Auto => {} - Self::Bool(v) => v.hash(state), - Self::Int(v) => v.hash(state), - Self::Float(v) => v.to_bits().hash(state), - Self::Length(v) => v.hash(state), - Self::Angle(v) => v.hash(state), - Self::Ratio(v) => v.hash(state), - Self::Relative(v) => v.hash(state), - Self::Fraction(v) => v.hash(state), - Self::Color(v) => v.hash(state), - Self::Symbol(v) => v.hash(state), - Self::Str(v) => v.hash(state), - Self::Bytes(v) => v.hash(state), - Self::Label(v) => v.hash(state), - Self::Content(v) => v.hash(state), - Self::Styles(v) => v.hash(state), - Self::Array(v) => v.hash(state), - Self::Dict(v) => v.hash(state), - Self::Func(v) => v.hash(state), - Self::Args(v) => v.hash(state), - Self::Module(v) => v.hash(state), - Self::Dyn(v) => v.hash(state), - } - } -} - -/// A dynamic value. -#[derive(Clone, Hash)] -#[allow(clippy::derived_hash_with_manual_eq)] -pub struct Dynamic(Arc); - -impl Dynamic { - /// Create a new instance from any value that satisfies the required bounds. - pub fn new(any: T) -> Self - where - T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, - { - Self(Arc::new(any)) - } - - /// Whether the wrapped type is `T`. - pub fn is(&self) -> bool { - (*self.0).as_any().is::() - } - - /// Try to downcast to a reference to a specific type. - pub fn downcast(&self) -> Option<&T> { - (*self.0).as_any().downcast_ref() - } - - /// The name of the stored value's type. - pub fn type_name(&self) -> &'static str { - self.0.dyn_type_name() - } -} - -impl Debug for Dynamic { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Debug::fmt(&self.0, f) - } -} - -impl PartialEq for Dynamic { - fn eq(&self, other: &Self) -> bool { - self.0.dyn_eq(other) - } -} - -cast! { - Dynamic, - self => Value::Dyn(self), -} - -trait Bounds: Debug + Sync + Send + 'static { - fn as_any(&self) -> &dyn Any; - fn dyn_eq(&self, other: &Dynamic) -> bool; - fn dyn_type_name(&self) -> &'static str; - fn hash128(&self) -> u128; -} - -impl Bounds for T -where - T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, -{ - fn as_any(&self) -> &dyn Any { - self - } - - fn dyn_eq(&self, other: &Dynamic) -> bool { - let Some(other) = other.downcast::() else { return false }; - self == other - } - - fn dyn_type_name(&self) -> &'static str { - T::TYPE_NAME - } - - #[tracing::instrument(skip_all)] - fn hash128(&self) -> u128 { - // Also hash the TypeId since values with different types but - // equal data should be different. - let mut state = SipHasher13::new(); - self.type_id().hash(&mut state); - self.hash(&mut state); - state.finish128().as_u128() - } -} - -impl Hash for dyn Bounds { - fn hash(&self, state: &mut H) { - state.write_u128(self.hash128()); - } -} - -/// The type of a value. -pub trait Type { - /// The name of the type. - const TYPE_NAME: &'static str; -} - -/// Implement traits for primitives. -macro_rules! primitive { - ( - $ty:ty: $name:literal, $variant:ident - $(, $other:ident$(($binding:ident))? => $out:expr)* - ) => { - impl Type for $ty { - const TYPE_NAME: &'static str = $name; - } - - impl Reflect for $ty { - fn describe() -> CastInfo { - CastInfo::Type(Self::TYPE_NAME) - } - - fn castable(value: &Value) -> bool { - matches!(value, Value::$variant(_) - $(| primitive!(@$other $(($binding))?))*) - } - } - - impl IntoValue for $ty { - fn into_value(self) -> Value { - Value::$variant(self) - } - } - - impl FromValue for $ty { - fn from_value(value: Value) -> StrResult { - match value { - Value::$variant(v) => Ok(v), - $(Value::$other$(($binding))? => Ok($out),)* - v => Err(eco_format!( - "expected {}, found {}", - Self::TYPE_NAME, - v.type_name(), - )), - } - } - } - }; - - (@$other:ident($binding:ident)) => { Value::$other(_) }; - (@$other:ident) => { Value::$other }; -} - -primitive! { bool: "boolean", Bool } -primitive! { i64: "integer", Int } -primitive! { f64: "float", Float, Int(v) => v as f64 } -primitive! { Length: "length", Length } -primitive! { Angle: "angle", Angle } -primitive! { Ratio: "ratio", Ratio } -primitive! { Rel: "relative length", - Relative, - Length(v) => v.into(), - Ratio(v) => v.into() -} -primitive! { Fr: "fraction", Fraction } -primitive! { Color: "color", Color } -primitive! { Symbol: "symbol", Symbol } -primitive! { - Str: "string", - Str, - Symbol(symbol) => symbol.get().into() -} -primitive! { Bytes: "bytes", Bytes } -primitive! { Label: "label", Label } -primitive! { Content: "content", - Content, - None => Content::empty(), - Symbol(v) => item!(text)(v.get().into()), - Str(v) => item!(text)(v.into()) -} -primitive! { Styles: "styles", Styles } -primitive! { Array: "array", Array } -primitive! { Dict: "dictionary", Dict } -primitive! { Func: "function", Func } -primitive! { Args: "arguments", Args } -primitive! { Module: "module", Module } - -#[cfg(test)] -mod tests { - use super::*; - use crate::eval::{array, dict}; - use crate::geom::RgbaColor; - - #[track_caller] - fn test(value: impl IntoValue, exp: &str) { - assert_eq!(format!("{:?}", value.into_value()), exp); - } - - #[test] - fn test_value_debug() { - // Primitives. - test(Value::None, "none"); - test(false, "false"); - test(12i64, "12"); - test(3.24, "3.24"); - test(Abs::pt(5.5), "5.5pt"); - test(Angle::deg(90.0), "90deg"); - test(Ratio::one() / 2.0, "50%"); - test(Ratio::new(0.3) + Length::from(Abs::cm(2.0)), "30% + 56.69pt"); - test(Fr::one() * 7.55, "7.55fr"); - test(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "rgb(\"#010101\")"); - - // Collections. - test("hello", r#""hello""#); - test("\n", r#""\n""#); - test("\\", r#""\\""#); - test("\"", r#""\"""#); - test(array![], "()"); - test(array![Value::None], "(none,)"); - test(array![1, 2], "(1, 2)"); - test(dict![], "(:)"); - test(dict!["one" => 1], "(one: 1)"); - test(dict!["two" => false, "one" => 1], "(two: false, one: 1)"); - } -} diff --git a/src/export/mod.rs b/src/export/mod.rs deleted file mode 100644 index eb0731a9..00000000 --- a/src/export/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Exporting into external formats. - -mod pdf; -mod render; - -pub use self::pdf::pdf; -pub use self::render::render; diff --git a/src/export/pdf/font.rs b/src/export/pdf/font.rs deleted file mode 100644 index f0676d8f..00000000 --- a/src/export/pdf/font.rs +++ /dev/null @@ -1,204 +0,0 @@ -use std::collections::BTreeMap; - -use ecow::{eco_format, EcoString}; -use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; -use pdf_writer::{Filter, Finish, Name, Rect, Str}; -use ttf_parser::{name_id, GlyphId, Tag}; -use unicode_general_category::GeneralCategory; - -use super::{deflate, EmExt, PdfContext, RefExt}; -use crate::font::Font; -use crate::util::{Bytes, SliceExt}; - -const CMAP_NAME: Name = Name(b"Custom"); -const SYSTEM_INFO: SystemInfo = SystemInfo { - registry: Str(b"Adobe"), - ordering: Str(b"Identity"), - supplement: 0, -}; - -/// Embed all used fonts into the PDF. -#[tracing::instrument(skip_all)] -pub fn write_fonts(ctx: &mut PdfContext) { - for font in ctx.font_map.items() { - let type0_ref = ctx.alloc.bump(); - let cid_ref = ctx.alloc.bump(); - let descriptor_ref = ctx.alloc.bump(); - let cmap_ref = ctx.alloc.bump(); - let data_ref = ctx.alloc.bump(); - ctx.font_refs.push(type0_ref); - - let glyph_set = ctx.glyph_sets.get_mut(font).unwrap(); - let metrics = font.metrics(); - let ttf = font.ttf(); - - let postscript_name = font - .find_name(name_id::POST_SCRIPT_NAME) - .unwrap_or_else(|| "unknown".to_string()); - - let base_font = eco_format!("ABCDEF+{}", postscript_name); - let base_font = Name(base_font.as_bytes()); - - // Write the base font object referencing the CID font. - ctx.writer - .type0_font(type0_ref) - .base_font(base_font) - .encoding_predefined(Name(b"Identity-H")) - .descendant_font(cid_ref) - .to_unicode(cmap_ref); - - // Check for the presence of CFF outlines to select the correct - // CID-Font subtype. - let subtype = match ttf - .raw_face() - .table(Tag::from_bytes(b"CFF ")) - .or(ttf.raw_face().table(Tag::from_bytes(b"CFF2"))) - { - Some(_) => CidFontType::Type0, - None => CidFontType::Type2, - }; - - // Write the CID font referencing the font descriptor. - let mut cid = ctx.writer.cid_font(cid_ref); - cid.subtype(subtype); - cid.base_font(base_font); - cid.system_info(SYSTEM_INFO); - cid.font_descriptor(descriptor_ref); - cid.default_width(0.0); - - if subtype == CidFontType::Type2 { - cid.cid_to_gid_map_predefined(Name(b"Identity")); - } - - // Extract the widths of all glyphs. - let num_glyphs = ttf.number_of_glyphs(); - let mut widths = vec![0.0; num_glyphs as usize]; - for &g in glyph_set.keys() { - let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0); - widths[g as usize] = font.to_em(x).to_font_units(); - } - - // Write all non-zero glyph widths. - let mut first = 0; - let mut width_writer = cid.widths(); - for (w, group) in widths.group_by_key(|&w| w) { - let end = first + group.len(); - if w != 0.0 { - let last = end - 1; - width_writer.same(first as u16, last as u16, w); - } - first = end; - } - - width_writer.finish(); - cid.finish(); - - let mut flags = FontFlags::empty(); - flags.set(FontFlags::SERIF, postscript_name.contains("Serif")); - flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); - flags.set(FontFlags::ITALIC, ttf.is_italic()); - flags.insert(FontFlags::SYMBOLIC); - flags.insert(FontFlags::SMALL_CAP); - - let global_bbox = ttf.global_bounding_box(); - let bbox = Rect::new( - font.to_em(global_bbox.x_min).to_font_units(), - font.to_em(global_bbox.y_min).to_font_units(), - font.to_em(global_bbox.x_max).to_font_units(), - font.to_em(global_bbox.y_max).to_font_units(), - ); - - let italic_angle = ttf.italic_angle().unwrap_or(0.0); - let ascender = metrics.ascender.to_font_units(); - let descender = metrics.descender.to_font_units(); - let cap_height = metrics.cap_height.to_font_units(); - let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); - - // Write the font descriptor (contains metrics about the font). - let mut font_descriptor = ctx.writer.font_descriptor(descriptor_ref); - font_descriptor - .name(base_font) - .flags(flags) - .bbox(bbox) - .italic_angle(italic_angle) - .ascent(ascender) - .descent(descender) - .cap_height(cap_height) - .stem_v(stem_v); - - match subtype { - CidFontType::Type0 => font_descriptor.font_file3(data_ref), - CidFontType::Type2 => font_descriptor.font_file2(data_ref), - }; - - font_descriptor.finish(); - - // Write the /ToUnicode character map, which maps glyph ids back to - // unicode codepoints to enable copying out of the PDF. - let cmap = create_cmap(ttf, glyph_set); - ctx.writer.cmap(cmap_ref, &cmap.finish()); - - // Subset and write the font's bytes. - let glyphs: Vec<_> = glyph_set.keys().copied().collect(); - let data = subset_font(font, &glyphs); - let mut stream = ctx.writer.stream(data_ref, &data); - stream.filter(Filter::FlateDecode); - - if subtype == CidFontType::Type0 { - stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C")); - } - - stream.finish(); - } -} - -/// Subset a font to the given glyphs. -#[comemo::memoize] -fn subset_font(font: &Font, glyphs: &[u16]) -> Bytes { - let data = font.data(); - let profile = subsetter::Profile::pdf(glyphs); - let subsetted = subsetter::subset(data, font.index(), profile); - let data = subsetted.as_deref().unwrap_or(data); - deflate(data).into() -} - -/// Create a /ToUnicode CMap. -fn create_cmap( - ttf: &ttf_parser::Face, - glyph_set: &mut BTreeMap, -) -> UnicodeCmap { - // For glyphs that have codepoints mapping to in the font's cmap table, we - // prefer them over pre-existing text mappings from the document. Only - // things that don't have a corresponding codepoint (or only a private-use - // one) like the "Th" in Linux Libertine get the text of their first - // occurrences in the document instead. - for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) { - if !subtable.is_unicode() { - continue; - } - - subtable.codepoints(|n| { - let Some(c) = std::char::from_u32(n) else { return }; - if unicode_general_category::get_general_category(c) - == GeneralCategory::PrivateUse - { - return; - } - - let Some(GlyphId(g)) = ttf.glyph_index(c) else { return }; - if glyph_set.contains_key(&g) { - glyph_set.insert(g, c.into()); - } - }); - } - - // Produce a reverse mapping from glyphs to unicode strings. - let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); - for (&g, text) in glyph_set.iter() { - if !text.is_empty() { - cmap.pair_with_multiple(g, text.chars()); - } - } - - cmap -} diff --git a/src/export/pdf/image.rs b/src/export/pdf/image.rs deleted file mode 100644 index 48472d9f..00000000 --- a/src/export/pdf/image.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::io::Cursor; - -use image::{DynamicImage, GenericImageView, Rgba}; -use pdf_writer::{Filter, Finish}; - -use super::{deflate, PdfContext, RefExt}; -use crate::image::{DecodedImage, Image, RasterFormat}; -use crate::util::Bytes; - -/// Embed all used images into the PDF. -#[tracing::instrument(skip_all)] -pub fn write_images(ctx: &mut PdfContext) { - for image in ctx.image_map.items() { - let image_ref = ctx.alloc.bump(); - let icc_ref = ctx.alloc.bump(); - ctx.image_refs.push(image_ref); - - let width = image.width(); - let height = image.height(); - - // Add the primary image. - // TODO: Error if image could not be encoded. - match image.decoded().as_ref() { - DecodedImage::Raster(dynamic, icc, _) => { - // TODO: Error if image could not be encoded. - let (data, filter, has_color) = encode_image(image); - let mut image = ctx.writer.image_xobject(image_ref, &data); - image.filter(filter); - image.width(width as i32); - image.height(height as i32); - image.bits_per_component(8); - - let space = image.color_space(); - if icc.is_some() { - space.icc_based(icc_ref); - } else if has_color { - space.device_rgb(); - } else { - space.device_gray(); - } - - // Add a second gray-scale image containing the alpha values if - // this image has an alpha channel. - if dynamic.color().has_alpha() { - let (alpha_data, alpha_filter) = encode_alpha(dynamic); - let mask_ref = ctx.alloc.bump(); - image.s_mask(mask_ref); - image.finish(); - - let mut mask = ctx.writer.image_xobject(mask_ref, &alpha_data); - mask.filter(alpha_filter); - mask.width(width as i32); - mask.height(height as i32); - mask.color_space().device_gray(); - mask.bits_per_component(8); - } else { - image.finish(); - } - - if let Some(icc) = icc { - let compressed = deflate(&icc.0); - let mut stream = ctx.writer.icc_profile(icc_ref, &compressed); - stream.filter(Filter::FlateDecode); - if has_color { - stream.n(3); - stream.alternate().srgb(); - } else { - stream.n(1); - stream.alternate().d65_gray(); - } - } - } - DecodedImage::Svg(svg) => { - let next_ref = svg2pdf::convert_tree_into( - svg, - svg2pdf::Options::default(), - &mut ctx.writer, - image_ref, - ); - ctx.alloc = next_ref; - } - } - } -} - -/// Encode an image with a suitable filter and return the data, filter and -/// whether the image has color. -/// -/// Skips the alpha channel as that's encoded separately. -#[comemo::memoize] -#[tracing::instrument(skip_all)] -fn encode_image(image: &Image) -> (Bytes, Filter, bool) { - let decoded = image.decoded(); - let (dynamic, format) = match decoded.as_ref() { - DecodedImage::Raster(dynamic, _, format) => (dynamic, *format), - _ => panic!("can only encode raster image"), - }; - - match (format, dynamic) { - // 8-bit gray JPEG. - (RasterFormat::Jpg, DynamicImage::ImageLuma8(_)) => { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (data.into_inner().into(), Filter::DctDecode, false) - } - - // 8-bit RGB JPEG (CMYK JPEGs get converted to RGB earlier). - (RasterFormat::Jpg, DynamicImage::ImageRgb8(_)) => { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (data.into_inner().into(), Filter::DctDecode, true) - } - - // TODO: Encode flate streams with PNG-predictor? - - // 8-bit gray PNG. - (RasterFormat::Png, DynamicImage::ImageLuma8(luma)) => { - let data = deflate(luma.as_raw()); - (data.into(), Filter::FlateDecode, false) - } - - // Anything else (including Rgb(a) PNGs). - (_, buf) => { - let (width, height) = buf.dimensions(); - let mut pixels = Vec::with_capacity(3 * width as usize * height as usize); - for (_, _, Rgba([r, g, b, _])) in buf.pixels() { - pixels.push(r); - pixels.push(g); - pixels.push(b); - } - - let data = deflate(&pixels); - (data.into(), Filter::FlateDecode, true) - } - } -} - -/// Encode an image's alpha channel if present. -#[tracing::instrument(skip_all)] -fn encode_alpha(dynamic: &DynamicImage) -> (Vec, Filter) { - let pixels: Vec<_> = dynamic.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); - (deflate(&pixels), Filter::FlateDecode) -} diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs deleted file mode 100644 index 48485862..00000000 --- a/src/export/pdf/mod.rs +++ /dev/null @@ -1,235 +0,0 @@ -//! Exporting into PDF documents. - -mod font; -mod image; -mod outline; -mod page; - -use std::cmp::Eq; -use std::collections::{BTreeMap, HashMap}; -use std::hash::Hash; - -use ecow::EcoString; -use pdf_writer::types::Direction; -use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr}; -use xmp_writer::{LangId, RenditionClass, XmpWriter}; - -use self::page::Page; -use crate::doc::{Document, Lang}; -use crate::font::Font; -use crate::geom::{Abs, Dir, Em}; -use crate::image::Image; -use crate::model::Introspector; - -/// Export a document into a PDF file. -/// -/// Returns the raw bytes making up the PDF file. -#[tracing::instrument(skip_all)] -pub fn pdf(document: &Document) -> Vec { - let mut ctx = PdfContext::new(document); - page::construct_pages(&mut ctx, &document.pages); - font::write_fonts(&mut ctx); - image::write_images(&mut ctx); - page::write_page_tree(&mut ctx); - write_catalog(&mut ctx); - ctx.writer.finish() -} - -/// Identifies the color space definitions. -const SRGB: Name<'static> = Name(b"srgb"); -const D65_GRAY: Name<'static> = Name(b"d65gray"); - -/// Context for exporting a whole PDF document. -pub struct PdfContext<'a> { - document: &'a Document, - introspector: Introspector, - writer: PdfWriter, - pages: Vec, - page_heights: Vec, - alloc: Ref, - page_tree_ref: Ref, - font_refs: Vec, - image_refs: Vec, - page_refs: Vec, - font_map: Remapper, - image_map: Remapper, - /// For each font a mapping from used glyphs to their text representation. - /// May contain multiple chars in case of ligatures or similar things. The - /// same glyph can have a different text representation within one document, - /// then we just save the first one. The resulting strings are used for the - /// PDF's /ToUnicode map for glyphs that don't have an entry in the font's - /// cmap. This is important for copy-paste and searching. - glyph_sets: HashMap>, - languages: HashMap, -} - -impl<'a> PdfContext<'a> { - fn new(document: &'a Document) -> Self { - let mut alloc = Ref::new(1); - let page_tree_ref = alloc.bump(); - Self { - document, - introspector: Introspector::new(&document.pages), - writer: PdfWriter::new(), - pages: vec![], - page_heights: vec![], - alloc, - page_tree_ref, - page_refs: vec![], - font_refs: vec![], - image_refs: vec![], - font_map: Remapper::new(), - image_map: Remapper::new(), - glyph_sets: HashMap::new(), - languages: HashMap::new(), - } - } -} - -/// Write the document catalog. -#[tracing::instrument(skip_all)] -fn write_catalog(ctx: &mut PdfContext) { - let lang = ctx - .languages - .iter() - .max_by_key(|(&lang, &count)| (count, lang)) - .map(|(&k, _)| k); - - let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { - Direction::R2L - } else { - Direction::L2R - }; - - // Write the outline tree. - let outline_root_id = outline::write_outline(ctx); - - // Write the document information. - let mut info = ctx.writer.document_info(ctx.alloc.bump()); - let mut xmp = XmpWriter::new(); - if let Some(title) = &ctx.document.title { - info.title(TextStr(title)); - xmp.title([(None, title.as_str())]); - } - - let authors = &ctx.document.author; - if !authors.is_empty() { - info.author(TextStr(&authors.join(", "))); - xmp.creator(authors.iter().map(|s| s.as_str())); - } - info.creator(TextStr("Typst")); - info.finish(); - xmp.creator_tool("Typst"); - xmp.num_pages(ctx.document.pages.len() as u32); - xmp.format("application/pdf"); - xmp.language(ctx.languages.keys().map(|lang| LangId(lang.as_str()))); - xmp.rendition_class(RenditionClass::Proof); - xmp.pdf_version("1.7"); - - let xmp_buf = xmp.finish(None); - let meta_ref = ctx.alloc.bump(); - let mut meta_stream = ctx.writer.stream(meta_ref, xmp_buf.as_bytes()); - meta_stream.pair(Name(b"Type"), Name(b"Metadata")); - meta_stream.pair(Name(b"Subtype"), Name(b"XML")); - meta_stream.finish(); - - // Write the document catalog. - let mut catalog = ctx.writer.catalog(ctx.alloc.bump()); - catalog.pages(ctx.page_tree_ref); - catalog.viewer_preferences().direction(dir); - catalog.pair(Name(b"Metadata"), meta_ref); - - if let Some(outline_root_id) = outline_root_id { - catalog.outlines(outline_root_id); - } - - if let Some(lang) = lang { - catalog.lang(TextStr(lang.as_str())); - } -} - -/// Compress data with the DEFLATE algorithm. -#[tracing::instrument(skip_all)] -fn deflate(data: &[u8]) -> Vec { - const COMPRESSION_LEVEL: u8 = 6; - miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) -} - -/// Assigns new, consecutive PDF-internal indices to items. -struct Remapper { - /// Forwards from the items to the pdf indices. - to_pdf: HashMap, - /// Backwards from the pdf indices to the items. - to_items: Vec, -} - -impl Remapper -where - T: Eq + Hash + Clone, -{ - fn new() -> Self { - Self { to_pdf: HashMap::new(), to_items: vec![] } - } - - fn insert(&mut self, item: T) { - let to_layout = &mut self.to_items; - self.to_pdf.entry(item.clone()).or_insert_with(|| { - let pdf_index = to_layout.len(); - to_layout.push(item); - pdf_index - }); - } - - fn map(&self, item: T) -> usize { - self.to_pdf[&item] - } - - fn pdf_indices<'a>( - &'a self, - refs: &'a [Ref], - ) -> impl Iterator + 'a { - refs.iter().copied().zip(0..self.to_pdf.len()) - } - - fn items(&self) -> impl Iterator + '_ { - self.to_items.iter() - } -} - -/// Additional methods for [`Abs`]. -trait AbsExt { - /// Convert an to a number of points. - fn to_f32(self) -> f32; -} - -impl AbsExt for Abs { - fn to_f32(self) -> f32 { - self.to_pt() as f32 - } -} - -/// Additional methods for [`Em`]. -trait EmExt { - /// Convert an em length to a number of PDF font units. - fn to_font_units(self) -> f32; -} - -impl EmExt for Em { - fn to_font_units(self) -> f32 { - 1000.0 * self.get() as f32 - } -} - -/// Additional methods for [`Ref`]. -trait RefExt { - /// Bump the reference up by one and return the previous one. - fn bump(&mut self) -> Self; -} - -impl RefExt for Ref { - fn bump(&mut self) -> Self { - let prev = *self; - *self = Self::new(prev.get() + 1); - prev - } -} diff --git a/src/export/pdf/outline.rs b/src/export/pdf/outline.rs deleted file mode 100644 index 539647eb..00000000 --- a/src/export/pdf/outline.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::num::NonZeroUsize; - -use pdf_writer::{Finish, Ref, TextStr}; - -use super::{AbsExt, PdfContext, RefExt}; -use crate::geom::Abs; -use crate::model::Content; - -/// Construct the outline for the document. -#[tracing::instrument(skip_all)] -pub fn write_outline(ctx: &mut PdfContext) -> Option { - let mut tree: Vec = vec![]; - for heading in ctx.introspector.query(&item!(heading_func).select()) { - let leaf = HeadingNode::leaf((*heading).clone()); - - let mut children = &mut tree; - while children.last().map_or(false, |last| last.level < leaf.level) { - children = &mut children.last_mut().unwrap().children; - } - - children.push(leaf); - } - - if tree.is_empty() { - return None; - } - - let root_id = ctx.alloc.bump(); - let start_ref = ctx.alloc; - let len = tree.len(); - - let mut prev_ref = None; - for (i, node) in tree.iter().enumerate() { - prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len)); - } - - ctx.writer - .outline(root_id) - .first(start_ref) - .last(Ref::new(ctx.alloc.get() - 1)) - .count(tree.len() as i32); - - Some(root_id) -} - -/// A heading in the outline panel. -#[derive(Debug, Clone)] -struct HeadingNode { - element: Content, - level: NonZeroUsize, - children: Vec, -} - -impl HeadingNode { - fn leaf(element: Content) -> Self { - HeadingNode { - level: element.expect_field::("level"), - element, - children: Vec::new(), - } - } - - fn len(&self) -> usize { - 1 + self.children.iter().map(Self::len).sum::() - } -} - -/// Write an outline item and all its children. -#[tracing::instrument(skip_all)] -fn write_outline_item( - ctx: &mut PdfContext, - node: &HeadingNode, - parent_ref: Ref, - prev_ref: Option, - is_last: bool, -) -> Ref { - let id = ctx.alloc.bump(); - let next_ref = Ref::new(id.get() + node.len() as i32); - - let mut outline = ctx.writer.outline_item(id); - outline.parent(parent_ref); - - if !is_last { - outline.next(next_ref); - } - - if let Some(prev_rev) = prev_ref { - outline.prev(prev_rev); - } - - if !node.children.is_empty() { - let current_child = Ref::new(id.get() + 1); - outline.first(current_child); - outline.last(Ref::new(next_ref.get() - 1)); - outline.count(-(node.children.len() as i32)); - } - - let body = node.element.expect_field::("body"); - outline.title(TextStr(body.plain_text().trim())); - - let loc = node.element.location().unwrap(); - let pos = ctx.introspector.position(loc); - let index = pos.page.get() - 1; - if let Some(&height) = ctx.page_heights.get(index) { - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - outline.dest().page(ctx.page_refs[index]).xyz( - pos.point.x.to_f32(), - height - y.to_f32(), - None, - ); - } - - outline.finish(); - - let mut prev_ref = None; - for (i, child) in node.children.iter().enumerate() { - prev_ref = Some(write_outline_item( - ctx, - child, - id, - prev_ref, - i + 1 == node.children.len(), - )); - } - - id -} diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs deleted file mode 100644 index 22e590d5..00000000 --- a/src/export/pdf/page.rs +++ /dev/null @@ -1,565 +0,0 @@ -use ecow::eco_format; -use pdf_writer::types::{ - ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle, -}; -use pdf_writer::writers::ColorSpace; -use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; - -use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB}; -use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem}; -use crate::font::Font; -use crate::geom::{ - self, Abs, Color, Em, Geometry, LineCap, LineJoin, Numeric, Paint, Point, Ratio, - Shape, Size, Stroke, Transform, -}; -use crate::image::Image; - -/// Construct page objects. -#[tracing::instrument(skip_all)] -pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { - for frame in frames { - construct_page(ctx, frame); - } -} - -/// Construct a page object. -#[tracing::instrument(skip_all)] -pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) { - let page_ref = ctx.alloc.bump(); - ctx.page_refs.push(page_ref); - ctx.page_heights.push(frame.height().to_f32()); - - let mut ctx = PageContext { - parent: ctx, - page_ref, - content: Content::new(), - state: State::default(), - saves: vec![], - bottom: 0.0, - links: vec![], - }; - - let size = frame.size(); - - // Make the coordinate system start at the top-left. - ctx.bottom = size.y.to_f32(); - ctx.transform(Transform { - sx: Ratio::one(), - ky: Ratio::zero(), - kx: Ratio::zero(), - sy: Ratio::new(-1.0), - tx: Abs::zero(), - ty: size.y, - }); - - // Encode the page into the content stream. - write_frame(&mut ctx, frame); - - let page = Page { - size, - content: ctx.content, - id: ctx.page_ref, - links: ctx.links, - }; - - ctx.parent.pages.push(page); -} - -/// Write the page tree. -#[tracing::instrument(skip_all)] -pub fn write_page_tree(ctx: &mut PdfContext) { - for page in std::mem::take(&mut ctx.pages).into_iter() { - write_page(ctx, page); - } - - let mut pages = ctx.writer.pages(ctx.page_tree_ref); - pages - .count(ctx.page_refs.len() as i32) - .kids(ctx.page_refs.iter().copied()); - - let mut resources = pages.resources(); - let mut spaces = resources.color_spaces(); - spaces.insert(SRGB).start::().srgb(); - spaces.insert(D65_GRAY).start::().d65_gray(); - spaces.finish(); - - let mut fonts = resources.fonts(); - for (font_ref, f) in ctx.font_map.pdf_indices(&ctx.font_refs) { - let name = eco_format!("F{}", f); - fonts.pair(Name(name.as_bytes()), font_ref); - } - - fonts.finish(); - - let mut images = resources.x_objects(); - for (image_ref, im) in ctx.image_map.pdf_indices(&ctx.image_refs) { - let name = eco_format!("Im{}", im); - images.pair(Name(name.as_bytes()), image_ref); - } - - images.finish(); - resources.finish(); - pages.finish(); -} - -/// Write a page tree node. -#[tracing::instrument(skip_all)] -fn write_page(ctx: &mut PdfContext, page: Page) { - let content_id = ctx.alloc.bump(); - - let mut page_writer = ctx.writer.page(page.id); - page_writer.parent(ctx.page_tree_ref); - - let w = page.size.x.to_f32(); - let h = page.size.y.to_f32(); - page_writer.media_box(Rect::new(0.0, 0.0, w, h)); - page_writer.contents(content_id); - - let mut annotations = page_writer.annotations(); - for (dest, rect) in page.links { - let mut annotation = annotations.push(); - annotation.subtype(AnnotationType::Link).rect(rect); - annotation.border(0.0, 0.0, 0.0, None); - - let pos = match dest { - Destination::Url(uri) => { - annotation - .action() - .action_type(ActionType::Uri) - .uri(Str(uri.as_bytes())); - continue; - } - Destination::Position(pos) => pos, - Destination::Location(loc) => ctx.introspector.position(loc), - }; - - let index = pos.page.get() - 1; - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - if let Some(&height) = ctx.page_heights.get(index) { - annotation - .action() - .action_type(ActionType::GoTo) - .destination() - .page(ctx.page_refs[index]) - .xyz(pos.point.x.to_f32(), height - y.to_f32(), None); - } - } - - annotations.finish(); - page_writer.finish(); - - let data = page.content.finish(); - let data = deflate(&data); - ctx.writer.stream(content_id, &data).filter(Filter::FlateDecode); -} - -/// Data for an exported page. -pub struct Page { - /// The indirect object id of the page. - pub id: Ref, - /// The page's dimensions. - pub size: Size, - /// The page's content stream. - pub content: Content, - /// Links in the PDF coordinate system. - pub links: Vec<(Destination, Rect)>, -} - -/// An exporter for the contents of a single PDF page. -struct PageContext<'a, 'b> { - parent: &'a mut PdfContext<'b>, - page_ref: Ref, - content: Content, - state: State, - saves: Vec, - bottom: f32, - links: Vec<(Destination, Rect)>, -} - -/// A simulated graphics state used to deduplicate graphics state changes and -/// keep track of the current transformation matrix for link annotations. -#[derive(Debug, Default, Clone)] -struct State { - transform: Transform, - font: Option<(Font, Abs)>, - fill: Option, - fill_space: Option>, - stroke: Option, - stroke_space: Option>, -} - -impl PageContext<'_, '_> { - fn save_state(&mut self) { - self.saves.push(self.state.clone()); - self.content.save_state(); - } - - fn restore_state(&mut self) { - self.content.restore_state(); - self.state = self.saves.pop().expect("missing state save"); - } - - fn transform(&mut self, transform: Transform) { - let Transform { sx, ky, kx, sy, tx, ty } = transform; - self.state.transform = self.state.transform.pre_concat(transform); - self.content.transform([ - sx.get() as _, - ky.get() as _, - kx.get() as _, - sy.get() as _, - tx.to_f32(), - ty.to_f32(), - ]); - } - - fn set_font(&mut self, font: &Font, size: Abs) { - if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) { - self.parent.font_map.insert(font.clone()); - let name = eco_format!("F{}", self.parent.font_map.map(font.clone())); - self.content.set_font(Name(name.as_bytes()), size.to_f32()); - self.state.font = Some((font.clone(), size)); - } - } - - fn set_fill(&mut self, fill: &Paint) { - if self.state.fill.as_ref() != Some(fill) { - let f = |c| c as f32 / 255.0; - let Paint::Solid(color) = fill; - match color { - Color::Luma(c) => { - self.set_fill_color_space(D65_GRAY); - self.content.set_fill_gray(f(c.0)); - } - Color::Rgba(c) => { - self.set_fill_color_space(SRGB); - self.content.set_fill_color([f(c.r), f(c.g), f(c.b)]); - } - Color::Cmyk(c) => { - self.reset_fill_color_space(); - self.content.set_fill_cmyk(f(c.c), f(c.m), f(c.y), f(c.k)); - } - } - self.state.fill = Some(fill.clone()); - } - } - - fn set_fill_color_space(&mut self, space: Name<'static>) { - if self.state.fill_space != Some(space) { - self.content.set_fill_color_space(ColorSpaceOperand::Named(space)); - self.state.fill_space = Some(space); - } - } - - fn reset_fill_color_space(&mut self) { - self.state.fill_space = None; - } - - fn set_stroke(&mut self, stroke: &Stroke) { - if self.state.stroke.as_ref() != Some(stroke) { - let Stroke { - paint, - thickness, - line_cap, - line_join, - dash_pattern, - miter_limit, - } = stroke; - - let f = |c| c as f32 / 255.0; - let Paint::Solid(color) = paint; - match color { - Color::Luma(c) => { - self.set_stroke_color_space(D65_GRAY); - self.content.set_stroke_gray(f(c.0)); - } - Color::Rgba(c) => { - self.set_stroke_color_space(SRGB); - self.content.set_stroke_color([f(c.r), f(c.g), f(c.b)]); - } - Color::Cmyk(c) => { - self.reset_stroke_color_space(); - self.content.set_stroke_cmyk(f(c.c), f(c.m), f(c.y), f(c.k)); - } - } - - self.content.set_line_width(thickness.to_f32()); - if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) { - self.content.set_line_cap(line_cap.into()); - } - if self.state.stroke.as_ref().map(|s| &s.line_join) != Some(line_join) { - self.content.set_line_join(line_join.into()); - } - if self.state.stroke.as_ref().map(|s| &s.dash_pattern) != Some(dash_pattern) { - if let Some(pattern) = dash_pattern { - self.content.set_dash_pattern( - pattern.array.iter().map(|l| l.to_f32()), - pattern.phase.to_f32(), - ); - } else { - self.content.set_dash_pattern([], 0.0); - } - } - if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) { - self.content.set_miter_limit(miter_limit.0 as f32); - } - self.state.stroke = Some(stroke.clone()); - } - } - - fn set_stroke_color_space(&mut self, space: Name<'static>) { - if self.state.stroke_space != Some(space) { - self.content.set_stroke_color_space(ColorSpaceOperand::Named(space)); - self.state.stroke_space = Some(space); - } - } - - fn reset_stroke_color_space(&mut self) { - self.state.stroke_space = None; - } -} - -/// Encode a frame into the content stream. -fn write_frame(ctx: &mut PageContext, frame: &Frame) { - for &(pos, ref item) in frame.items() { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - match item { - FrameItem::Group(group) => write_group(ctx, pos, group), - FrameItem::Text(text) => write_text(ctx, x, y, text), - FrameItem::Shape(shape, _) => write_shape(ctx, x, y, shape), - FrameItem::Image(image, size, _) => write_image(ctx, x, y, image, *size), - FrameItem::Meta(meta, size) => match meta { - Meta::Link(dest) => write_link(ctx, pos, dest, *size), - Meta::Elem(_) => {} - Meta::Hide => {} - Meta::PageNumbering(_) => {} - }, - } - } -} - -/// Encode a group into the content stream. -fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) { - let translation = Transform::translate(pos.x, pos.y); - - ctx.save_state(); - ctx.transform(translation.pre_concat(group.transform)); - - if group.clips { - let size = group.frame.size(); - let w = size.x.to_f32(); - let h = size.y.to_f32(); - ctx.content.move_to(0.0, 0.0); - ctx.content.line_to(w, 0.0); - ctx.content.line_to(w, h); - ctx.content.line_to(0.0, h); - ctx.content.clip_nonzero(); - ctx.content.end_path(); - } - - write_frame(ctx, &group.frame); - ctx.restore_state(); -} - -/// Encode a text run into the content stream. -fn write_text(ctx: &mut PageContext, x: f32, y: f32, text: &TextItem) { - *ctx.parent.languages.entry(text.lang).or_insert(0) += text.glyphs.len(); - - let glyph_set = ctx.parent.glyph_sets.entry(text.font.clone()).or_default(); - for g in &text.glyphs { - let segment = &text.text[g.range()]; - glyph_set.entry(g.id).or_insert_with(|| segment.into()); - } - - ctx.set_fill(&text.fill); - ctx.set_font(&text.font, text.size); - ctx.content.begin_text(); - - // Positiosn the text. - ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); - - let mut positioned = ctx.content.show_positioned(); - let mut items = positioned.items(); - let mut adjustment = Em::zero(); - let mut encoded = vec![]; - - // Write the glyphs with kerning adjustments. - for glyph in &text.glyphs { - adjustment += glyph.x_offset; - - if !adjustment.is_zero() { - if !encoded.is_empty() { - items.show(Str(&encoded)); - encoded.clear(); - } - - items.adjust(-adjustment.to_font_units()); - adjustment = Em::zero(); - } - - encoded.push((glyph.id >> 8) as u8); - encoded.push((glyph.id & 0xff) as u8); - - if let Some(advance) = text.font.advance(glyph.id) { - adjustment += glyph.x_advance - advance; - } - - adjustment -= glyph.x_offset; - } - - if !encoded.is_empty() { - items.show(Str(&encoded)); - } - - items.finish(); - positioned.finish(); - ctx.content.end_text(); -} - -/// Encode a geometrical shape into the content stream. -fn write_shape(ctx: &mut PageContext, x: f32, y: f32, shape: &Shape) { - let stroke = shape.stroke.as_ref().and_then(|stroke| { - if stroke.thickness.to_f32() > 0.0 { - Some(stroke) - } else { - None - } - }); - - if shape.fill.is_none() && stroke.is_none() { - return; - } - - if let Some(fill) = &shape.fill { - ctx.set_fill(fill); - } - - if let Some(stroke) = stroke { - ctx.set_stroke(stroke); - } - - match shape.geometry { - Geometry::Line(target) => { - let dx = target.x.to_f32(); - let dy = target.y.to_f32(); - ctx.content.move_to(x, y); - ctx.content.line_to(x + dx, y + dy); - } - Geometry::Rect(size) => { - let w = size.x.to_f32(); - let h = size.y.to_f32(); - if w > 0.0 && h > 0.0 { - ctx.content.rect(x, y, w, h); - } - } - Geometry::Path(ref path) => { - write_path(ctx, x, y, path); - } - } - - match (&shape.fill, stroke) { - (None, None) => unreachable!(), - (Some(_), None) => ctx.content.fill_nonzero(), - (None, Some(_)) => ctx.content.stroke(), - (Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(), - }; -} - -/// Encode a bezier path into the content stream. -fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) { - for elem in &path.0 { - match elem { - geom::PathItem::MoveTo(p) => { - ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()) - } - geom::PathItem::LineTo(p) => { - ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()) - } - geom::PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to( - x + p1.x.to_f32(), - y + p1.y.to_f32(), - x + p2.x.to_f32(), - y + p2.y.to_f32(), - x + p3.x.to_f32(), - y + p3.y.to_f32(), - ), - geom::PathItem::ClosePath => ctx.content.close_path(), - }; - } -} - -/// Encode a vector or raster image into the content stream. -fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) { - ctx.parent.image_map.insert(image.clone()); - let name = eco_format!("Im{}", ctx.parent.image_map.map(image.clone())); - let w = size.x.to_f32(); - let h = size.y.to_f32(); - ctx.content.save_state(); - ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]); - - if let Some(alt) = image.alt() { - let mut image_span = - ctx.content.begin_marked_content_with_properties(Name(b"Span")); - let mut image_alt = image_span.properties(); - image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes())); - image_alt.finish(); - image_span.finish(); - - ctx.content.x_object(Name(name.as_bytes())); - ctx.content.end_marked_content(); - } else { - ctx.content.x_object(Name(name.as_bytes())); - } - - ctx.content.restore_state(); -} - -/// Save a link for later writing in the annotations dictionary. -fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) { - let mut min_x = Abs::inf(); - let mut min_y = Abs::inf(); - let mut max_x = -Abs::inf(); - let mut max_y = -Abs::inf(); - - // Compute the bounding box of the transformed link. - for point in [ - pos, - pos + Point::with_x(size.x), - pos + Point::with_y(size.y), - pos + size.to_point(), - ] { - let t = point.transform(ctx.state.transform); - min_x.set_min(t.x); - min_y.set_min(t.y); - max_x.set_max(t.x); - max_y.set_max(t.y); - } - - let x1 = min_x.to_f32(); - let x2 = max_x.to_f32(); - let y1 = max_y.to_f32(); - let y2 = min_y.to_f32(); - let rect = Rect::new(x1, y1, x2, y2); - - ctx.links.push((dest.clone(), rect)); -} - -impl From<&LineCap> for LineCapStyle { - fn from(line_cap: &LineCap) -> Self { - match line_cap { - LineCap::Butt => LineCapStyle::ButtCap, - LineCap::Round => LineCapStyle::RoundCap, - LineCap::Square => LineCapStyle::ProjectingSquareCap, - } - } -} - -impl From<&LineJoin> for LineJoinStyle { - fn from(line_join: &LineJoin) -> Self { - match line_join { - LineJoin::Miter => LineJoinStyle::MiterJoin, - LineJoin::Round => LineJoinStyle::RoundJoin, - LineJoin::Bevel => LineJoinStyle::BevelJoin, - } - } -} diff --git a/src/export/render.rs b/src/export/render.rs deleted file mode 100644 index d8115b12..00000000 --- a/src/export/render.rs +++ /dev/null @@ -1,673 +0,0 @@ -//! Rendering into raster images. - -use std::io::Read; -use std::sync::Arc; - -use image::imageops::FilterType; -use image::{GenericImageView, Rgba}; -use pixglyph::Bitmap; -use resvg::FitTo; -use tiny_skia as sk; -use ttf_parser::{GlyphId, OutlineBuilder}; -use usvg::{NodeExt, TreeParsing}; - -use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem}; -use crate::font::Font; -use crate::geom::{ - self, Abs, Color, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Stroke, - Transform, -}; -use crate::image::{DecodedImage, Image}; - -/// Export a frame into a raster image. -/// -/// This renders the frame at the given number of pixels per point and returns -/// the resulting `tiny-skia` pixel buffer. -pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap { - let size = frame.size(); - let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32; - let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32; - - let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); - canvas.fill(fill.into()); - - let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); - render_frame(&mut canvas, ts, None, frame); - - canvas -} - -/// Render a frame into the canvas. -fn render_frame( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: Option<&sk::Mask>, - frame: &Frame, -) { - for (pos, item) in frame.items() { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - let ts = ts.pre_translate(x, y); - - match item { - FrameItem::Group(group) => { - render_group(canvas, ts, mask, group); - } - FrameItem::Text(text) => { - render_text(canvas, ts, mask, text); - } - FrameItem::Shape(shape, _) => { - render_shape(canvas, ts, mask, shape); - } - FrameItem::Image(image, size, _) => { - render_image(canvas, ts, mask, image, *size); - } - FrameItem::Meta(meta, _) => match meta { - Meta::Link(_) => {} - Meta::Elem(_) => {} - Meta::PageNumbering(_) => {} - Meta::Hide => {} - }, - } - } -} - -/// Render a group frame with optional transform and clipping into the canvas. -fn render_group( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: Option<&sk::Mask>, - group: &GroupItem, -) { - let ts = ts.pre_concat(group.transform.into()); - - let mut mask = mask; - let storage; - if group.clips { - let size = group.frame.size(); - let w = size.x.to_f32(); - let h = size.y.to_f32(); - if let Some(path) = sk::Rect::from_xywh(0.0, 0.0, w, h) - .map(sk::PathBuilder::from_rect) - .and_then(|path| path.transform(ts)) - { - if let Some(mask) = mask { - let mut mask = mask.clone(); - mask.intersect_path( - &path, - sk::FillRule::default(), - false, - sk::Transform::default(), - ); - storage = mask; - } else { - let pxw = canvas.width(); - let pxh = canvas.height(); - let Some(mut mask) = sk::Mask::new(pxw, pxh) else { - // Fails if clipping rect is empty. In that case we just - // clip everything by returning. - return; - }; - - mask.fill_path( - &path, - sk::FillRule::default(), - false, - sk::Transform::default(), - ); - storage = mask; - }; - - mask = Some(&storage); - } - } - - render_frame(canvas, ts, mask, &group.frame); -} - -/// Render a text run into the canvas. -fn render_text( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: Option<&sk::Mask>, - text: &TextItem, -) { - let mut x = 0.0; - for glyph in &text.glyphs { - let id = GlyphId(glyph.id); - let offset = x + glyph.x_offset.at(text.size).to_f32(); - let ts = ts.pre_translate(offset, 0.0); - - render_svg_glyph(canvas, ts, mask, text, id) - .or_else(|| render_bitmap_glyph(canvas, ts, mask, text, id)) - .or_else(|| render_outline_glyph(canvas, ts, mask, text, id)); - - x += glyph.x_advance.at(text.size).to_f32(); - } -} - -/// Render an SVG glyph into the canvas. -fn render_svg_glyph( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: Option<&sk::Mask>, - text: &TextItem, - id: GlyphId, -) -> Option<()> { - let mut data = text.font.ttf().glyph_svg_image(id)?; - - // Decompress SVGZ. - let mut decoded = vec![]; - if data.starts_with(&[0x1f, 0x8b]) { - let mut decoder = flate2::read::GzDecoder::new(data); - decoder.read_to_end(&mut decoded).ok()?; - data = &decoded; - } - - // Parse XML. - let xml = std::str::from_utf8(data).ok()?; - let document = roxmltree::Document::parse(xml).ok()?; - let root = document.root_element(); - - // Parse SVG. - let opts = usvg::Options::default(); - let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?; - let view_box = tree.view_box.rect; - - // If there's no viewbox defined, use the em square for our scale - // transformation ... - let upem = text.font.units_per_em() as f32; - let (mut width, mut height) = (upem, upem); - - // ... but if there's a viewbox or width, use that. - if root.has_attribute("viewBox") || root.has_attribute("width") { - width = view_box.width() as f32; - } - - // Same as for width. - if root.has_attribute("viewBox") || root.has_attribute("height") { - height = view_box.height() as f32; - } - - let size = text.size.to_f32(); - let ts = ts.pre_scale(size / width, size / height); - - // Compute the space we need to draw our glyph. - // See https://github.com/RazrFalcon/resvg/issues/602 for why - // using the svg size is problematic here. - let mut bbox = usvg::Rect::new_bbox(); - for node in tree.root.descendants() { - if let Some(rect) = node.calculate_bbox().and_then(|b| b.to_rect()) { - bbox = bbox.expand(rect); - } - } - - let canvas_rect = usvg::ScreenRect::new(0, 0, canvas.width(), canvas.height())?; - - // Compute the bbox after the transform is applied. - // We add a nice 5px border along the bounding box to - // be on the safe size. We also compute the intersection - // with the canvas rectangle - let svg_ts = usvg::Transform::new( - ts.sx.into(), - ts.kx.into(), - ts.ky.into(), - ts.sy.into(), - ts.tx.into(), - ts.ty.into(), - ); - let bbox = bbox.transform(&svg_ts)?.to_screen_rect(); - let bbox = usvg::ScreenRect::new( - bbox.left() - 5, - bbox.y() - 5, - bbox.width() + 10, - bbox.height() + 10, - )? - .fit_to_rect(canvas_rect); - - let mut pixmap = sk::Pixmap::new(bbox.width(), bbox.height())?; - - // We offset our transform so that the pixmap starts at the edge of the bbox. - let ts = ts.post_translate(-bbox.left() as f32, -bbox.top() as f32); - resvg::render(&tree, FitTo::Original, ts, pixmap.as_mut())?; - - canvas.draw_pixmap( - bbox.left(), - bbox.top(), - pixmap.as_ref(), - &sk::PixmapPaint::default(), - sk::Transform::identity(), - mask, - ); - - Some(()) -} - -/// Render a bitmap glyph into the canvas. -fn render_bitmap_glyph( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: Option<&sk::Mask>, - text: &TextItem, - id: GlyphId, -) -> Option<()> { - let size = text.size.to_f32(); - let ppem = size * ts.sy; - let raster = text.font.ttf().glyph_raster_image(id, ppem as u16)?; - let image = Image::new(raster.data.into(), raster.format.into(), None).ok()?; - - // FIXME: Vertical alignment isn't quite right for Apple Color Emoji, - // and maybe also for Noto Color Emoji. And: Is the size calculation - // correct? - let h = text.size; - let w = (image.width() as f64 / image.height() as f64) * h; - let dx = (raster.x as f32) / (image.width() as f32) * size; - let dy = (raster.y as f32) / (image.height() as f32) * size; - let ts = ts.pre_translate(dx, -size - dy); - render_image(canvas, ts, mask, &image, Size::new(w, h)) -} - -/// Render an outline glyph into the canvas. This is the "normal" case. -fn render_outline_glyph( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: Option<&sk::Mask>, - text: &TextItem, - id: GlyphId, -) -> Option<()> { - let ppem = text.size.to_f32() * ts.sy; - - // Render a glyph directly as a path. This only happens when the fast glyph - // rasterization can't be used due to very large text size or weird - // scale/skewing transforms. - if ppem > 100.0 || ts.kx != 0.0 || ts.ky != 0.0 || ts.sx != ts.sy { - let path = { - let mut builder = WrappedPathBuilder(sk::PathBuilder::new()); - text.font.ttf().outline_glyph(id, &mut builder)?; - builder.0.finish()? - }; - - let paint = (&text.fill).into(); - let rule = sk::FillRule::default(); - - // Flip vertically because font design coordinate - // system is Y-up. - let scale = text.size.to_f32() / text.font.units_per_em() as f32; - let ts = ts.pre_scale(scale, -scale); - canvas.fill_path(&path, &paint, rule, ts, mask); - return Some(()); - } - - // Rasterize the glyph with `pixglyph`. - #[comemo::memoize] - fn rasterize( - font: &Font, - id: GlyphId, - x: u32, - y: u32, - size: u32, - ) -> Option> { - let glyph = pixglyph::Glyph::load(font.ttf(), id)?; - Some(Arc::new(glyph.rasterize( - f32::from_bits(x), - f32::from_bits(y), - f32::from_bits(size), - ))) - } - - // Try to retrieve a prepared glyph or prepare it from scratch if it - // doesn't exist, yet. - let bitmap = - rasterize(&text.font, id, ts.tx.to_bits(), ts.ty.to_bits(), ppem.to_bits())?; - - // If we have a clip mask we first render to a pixmap that we then blend - // with our canvas - if mask.is_some() { - let mw = bitmap.width; - let mh = bitmap.height; - - let Paint::Solid(color) = text.fill; - let c = color.to_rgba(); - - // Pad the pixmap with 1 pixel in each dimension so that we do - // not get any problem with floating point errors along their border - let mut pixmap = sk::Pixmap::new(mw + 2, mh + 2)?; - for x in 0..mw { - for y in 0..mh { - let alpha = bitmap.coverage[(y * mw + x) as usize]; - let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, alpha).premultiply(); - pixmap.pixels_mut()[((y + 1) * (mw + 2) + (x + 1)) as usize] = color; - } - } - - let left = bitmap.left; - let top = bitmap.top; - - canvas.draw_pixmap( - left - 1, - top - 1, - pixmap.as_ref(), - &sk::PixmapPaint::default(), - sk::Transform::identity(), - mask, - ); - } else { - let cw = canvas.width() as i32; - let ch = canvas.height() as i32; - let mw = bitmap.width as i32; - let mh = bitmap.height as i32; - - // Determine the pixel bounding box that we actually need to draw. - let left = bitmap.left; - let right = left + mw; - let top = bitmap.top; - let bottom = top + mh; - - // Premultiply the text color. - let Paint::Solid(color) = text.fill; - let c = color.to_rgba(); - let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply().get(); - - // Blend the glyph bitmap with the existing pixels on the canvas. - let pixels = bytemuck::cast_slice_mut::(canvas.data_mut()); - for x in left.clamp(0, cw)..right.clamp(0, cw) { - for y in top.clamp(0, ch)..bottom.clamp(0, ch) { - let ai = ((y - top) * mw + (x - left)) as usize; - let cov = bitmap.coverage[ai]; - if cov == 0 { - continue; - } - - let pi = (y * cw + x) as usize; - if cov == 255 { - pixels[pi] = color; - continue; - } - - let applied = alpha_mul(color, cov as u32); - pixels[pi] = blend_src_over(applied, pixels[pi]); - } - } - } - - Some(()) -} - -/// Render a geometrical shape into the canvas. -fn render_shape( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: Option<&sk::Mask>, - shape: &Shape, -) -> Option<()> { - let path = match shape.geometry { - Geometry::Line(target) => { - let mut builder = sk::PathBuilder::new(); - builder.line_to(target.x.to_f32(), target.y.to_f32()); - builder.finish()? - } - Geometry::Rect(size) => { - let w = size.x.to_f32(); - let h = size.y.to_f32(); - let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?; - sk::PathBuilder::from_rect(rect) - } - Geometry::Path(ref path) => convert_path(path)?, - }; - - if let Some(fill) = &shape.fill { - let mut paint: sk::Paint = fill.into(); - if matches!(shape.geometry, Geometry::Rect(_)) { - paint.anti_alias = false; - } - - let rule = sk::FillRule::default(); - canvas.fill_path(&path, &paint, rule, ts, mask); - } - - if let Some(Stroke { - paint, - thickness, - line_cap, - line_join, - dash_pattern, - miter_limit, - }) = &shape.stroke - { - let width = thickness.to_f32(); - - // Don't draw zero-pt stroke. - if width > 0.0 { - let dash = dash_pattern.as_ref().and_then(|pattern| { - // tiny-skia only allows dash patterns with an even number of elements, - // while pdf allows any number. - let pattern_len = pattern.array.len(); - let len = - if pattern_len % 2 == 1 { 2 * pattern_len } else { pattern_len }; - let dash_array = - pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect(); - - sk::StrokeDash::new(dash_array, pattern.phase.to_f32()) - }); - let paint = paint.into(); - let stroke = sk::Stroke { - width, - line_cap: line_cap.into(), - line_join: line_join.into(), - dash, - miter_limit: miter_limit.0 as f32, - }; - canvas.stroke_path(&path, &paint, &stroke, ts, mask); - } - } - - Some(()) -} - -/// Convert a Typst path into a tiny-skia path. -fn convert_path(path: &geom::Path) -> Option { - let mut builder = sk::PathBuilder::new(); - for elem in &path.0 { - match elem { - PathItem::MoveTo(p) => { - builder.move_to(p.x.to_f32(), p.y.to_f32()); - } - PathItem::LineTo(p) => { - builder.line_to(p.x.to_f32(), p.y.to_f32()); - } - PathItem::CubicTo(p1, p2, p3) => { - builder.cubic_to( - p1.x.to_f32(), - p1.y.to_f32(), - p2.x.to_f32(), - p2.y.to_f32(), - p3.x.to_f32(), - p3.y.to_f32(), - ); - } - PathItem::ClosePath => { - builder.close(); - } - }; - } - builder.finish() -} - -/// Render a raster or SVG image into the canvas. -fn render_image( - canvas: &mut sk::Pixmap, - ts: sk::Transform, - mask: Option<&sk::Mask>, - image: &Image, - size: Size, -) -> Option<()> { - let view_width = size.x.to_f32(); - let view_height = size.y.to_f32(); - - // For better-looking output, resize `image` to its final size before - // painting it to `canvas`. For the math, see: - // https://github.com/typst/typst/issues/1404#issuecomment-1598374652 - let theta = f32::atan2(-ts.kx, ts.sx); - - // To avoid division by 0, choose the one of { sin, cos } that is - // further from 0. - let prefer_sin = theta.sin().abs() > std::f32::consts::FRAC_1_SQRT_2; - let scale_x = - f32::abs(if prefer_sin { ts.kx / theta.sin() } else { ts.sx / theta.cos() }); - - let aspect = (image.width() as f32) / (image.height() as f32); - let w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32; - let h = ((w as f32) / aspect).ceil() as u32; - - let pixmap = scaled_texture(image, w, h)?; - let paint_scale_x = view_width / pixmap.width() as f32; - let paint_scale_y = view_height / pixmap.height() as f32; - - let paint = sk::Paint { - shader: sk::Pattern::new( - (*pixmap).as_ref(), - sk::SpreadMode::Pad, - sk::FilterQuality::Nearest, - 1.0, - sk::Transform::from_scale(paint_scale_x, paint_scale_y), - ), - ..Default::default() - }; - - let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height)?; - canvas.fill_rect(rect, &paint, ts, mask); - - Some(()) -} - -/// Prepare a texture for an image at a scaled size. -#[comemo::memoize] -fn scaled_texture(image: &Image, w: u32, h: u32) -> Option> { - let mut pixmap = sk::Pixmap::new(w, h)?; - match image.decoded().as_ref() { - DecodedImage::Raster(dynamic, _, _) => { - let downscale = w < image.width(); - let filter = - if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom }; - let buf = dynamic.resize(w, h, filter); - for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { - let Rgba([r, g, b, a]) = src; - *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); - } - } - DecodedImage::Svg(tree) => { - resvg::render( - tree, - FitTo::Size(w, h), - sk::Transform::identity(), - pixmap.as_mut(), - )?; - } - } - Some(Arc::new(pixmap)) -} - -impl From for sk::Transform { - fn from(transform: Transform) -> Self { - let Transform { sx, ky, kx, sy, tx, ty } = transform; - sk::Transform::from_row( - sx.get() as _, - ky.get() as _, - kx.get() as _, - sy.get() as _, - tx.to_f32(), - ty.to_f32(), - ) - } -} - -impl From<&Paint> for sk::Paint<'static> { - fn from(paint: &Paint) -> Self { - let mut sk_paint = sk::Paint::default(); - let Paint::Solid(color) = *paint; - sk_paint.set_color(color.into()); - sk_paint.anti_alias = true; - sk_paint - } -} - -impl From for sk::Color { - fn from(color: Color) -> Self { - let c = color.to_rgba(); - sk::Color::from_rgba8(c.r, c.g, c.b, c.a) - } -} - -impl From<&LineCap> for sk::LineCap { - fn from(line_cap: &LineCap) -> Self { - match line_cap { - LineCap::Butt => sk::LineCap::Butt, - LineCap::Round => sk::LineCap::Round, - LineCap::Square => sk::LineCap::Square, - } - } -} - -impl From<&LineJoin> for sk::LineJoin { - fn from(line_join: &LineJoin) -> Self { - match line_join { - LineJoin::Miter => sk::LineJoin::Miter, - LineJoin::Round => sk::LineJoin::Round, - LineJoin::Bevel => sk::LineJoin::Bevel, - } - } -} - -/// Allows to build tiny-skia paths from glyph outlines. -struct WrappedPathBuilder(sk::PathBuilder); - -impl OutlineBuilder for WrappedPathBuilder { - fn move_to(&mut self, x: f32, y: f32) { - self.0.move_to(x, y); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.0.line_to(x, y); - } - - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - self.0.quad_to(x1, y1, x, y); - } - - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - self.0.cubic_to(x1, y1, x2, y2, x, y); - } - - fn close(&mut self) { - self.0.close(); - } -} - -/// Additional methods for [`Length`]. -trait AbsExt { - /// Convert to a number of points as f32. - fn to_f32(self) -> f32; -} - -impl AbsExt for Abs { - fn to_f32(self) -> f32 { - self.to_pt() as f32 - } -} - -// Alpha multiplication and blending are ported from: -// https://skia.googlesource.com/skia/+/refs/heads/main/include/core/SkColorPriv.h - -/// Blends two premulitplied, packed 32-bit RGBA colors. Alpha channel must be -/// in the 8 high bits. -fn blend_src_over(src: u32, dst: u32) -> u32 { - src + alpha_mul(dst, 256 - (src >> 24)) -} - -/// Alpha multiply a color. -fn alpha_mul(color: u32, scale: u32) -> u32 { - let mask = 0xff00ff; - let rb = ((color & mask) * scale) >> 8; - let ag = ((color >> 8) & mask) * scale; - (rb & mask) | (ag & !mask) -} diff --git a/src/file.rs b/src/file.rs deleted file mode 100644 index 8aaa746b..00000000 --- a/src/file.rs +++ /dev/null @@ -1,303 +0,0 @@ -//! File and package management. - -use std::collections::HashMap; -use std::fmt::{self, Debug, Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::RwLock; - -use ecow::{eco_format, EcoString}; -use once_cell::sync::Lazy; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -use crate::diag::{bail, FileError, StrResult}; -use crate::syntax::is_ident; -use crate::util::PathExt; - -/// The global package-path interner. -static INTERNER: Lazy> = - Lazy::new(|| RwLock::new(Interner { to_id: HashMap::new(), from_id: Vec::new() })); - -/// A package-path interner. -struct Interner { - to_id: HashMap, - from_id: Vec, -} - -/// An interned pair of a package specification and a path. -type Pair = &'static (Option, PathBuf); - -/// Identifies a file. -/// -/// This type is globally interned and thus cheap to copy, compare, and hash. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct FileId(u16); - -impl FileId { - /// Create a new interned file specification. - /// - /// The path must start with a `/` or this function will panic. - /// Note that the path is normalized before interning. - #[track_caller] - pub fn new(package: Option, path: &Path) -> Self { - assert_eq!( - path.components().next(), - Some(std::path::Component::RootDir), - "file path must be absolute within project or package: {}", - path.display(), - ); - - // Try to find an existing entry that we can reuse. - let pair = (package, path.normalize()); - if let Some(&id) = INTERNER.read().unwrap().to_id.get(&pair) { - return id; - } - - let mut interner = INTERNER.write().unwrap(); - let len = interner.from_id.len(); - if len >= usize::from(u16::MAX) { - panic!("too many file specifications"); - } - - // Create a new entry forever by leaking the pair. We can't leak more - // than 2^16 pair (and typically will leak a lot less), so its not a - // big deal. - let id = FileId(len as u16); - let leaked = Box::leak(Box::new(pair)); - interner.to_id.insert(leaked, id); - interner.from_id.push(leaked); - id - } - - /// Get an id that does not identify any real file. - pub const fn detached() -> Self { - Self(u16::MAX) - } - - /// Whether the id is the detached. - pub const fn is_detached(self) -> bool { - self.0 == Self::detached().0 - } - - /// The package the file resides in, if any. - pub fn package(&self) -> Option<&'static PackageSpec> { - if self.is_detached() { - None - } else { - self.pair().0.as_ref() - } - } - - /// The absolute and normalized path to the file _within_ the project or - /// package. - pub fn path(&self) -> &'static Path { - if self.is_detached() { - Path::new("/detached.typ") - } else { - &self.pair().1 - } - } - - /// Resolve a file location relative to this file. - pub fn join(self, path: &str) -> StrResult { - if self.is_detached() { - bail!("cannot access file system from here"); - } - - let package = self.package().cloned(); - let base = self.path(); - Ok(if let Some(parent) = base.parent() { - Self::new(package, &parent.join(path)) - } else { - Self::new(package, Path::new(path)) - }) - } - - /// Construct from a raw number. - pub(crate) const fn from_u16(v: u16) -> Self { - Self(v) - } - - /// Extract the raw underlying number. - pub(crate) const fn as_u16(self) -> u16 { - self.0 - } - - /// Get the static pair. - fn pair(&self) -> Pair { - INTERNER.read().unwrap().from_id[usize::from(self.0)] - } -} - -impl Display for FileId { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let path = self.path().display(); - match self.package() { - Some(package) => write!(f, "{package}{path}"), - None => write!(f, "{path}"), - } - } -} - -impl Debug for FileId { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -/// Identifies a package. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct PackageSpec { - /// The namespace the package lives in. - pub namespace: EcoString, - /// The name of the package within its namespace. - pub name: EcoString, - /// The package's version. - pub version: Version, -} - -impl FromStr for PackageSpec { - type Err = EcoString; - - fn from_str(s: &str) -> Result { - let mut s = unscanny::Scanner::new(s); - if !s.eat_if('@') { - bail!("package specification must start with '@'"); - } - - let namespace = s.eat_until('/'); - if namespace.is_empty() { - bail!("package specification is missing namespace"); - } else if !is_ident(namespace) { - bail!("`{namespace}` is not a valid package namespace"); - } - - s.eat_if('/'); - - let name = s.eat_until(':'); - if name.is_empty() { - bail!("package specification is missing name"); - } else if !is_ident(name) { - bail!("`{name}` is not a valid package name"); - } - - s.eat_if(':'); - - let version = s.after(); - if version.is_empty() { - bail!("package specification is missing version"); - } - - Ok(Self { - namespace: namespace.into(), - name: name.into(), - version: version.parse()?, - }) - } -} - -impl Display for PackageSpec { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "@{}/{}:{}", self.namespace, self.name, self.version) - } -} - -/// A package's version. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Version { - /// The package's major version. - pub major: u32, - /// The package's minor version. - pub minor: u32, - /// The package's patch version. - pub patch: u32, -} - -impl FromStr for Version { - type Err = EcoString; - - fn from_str(s: &str) -> Result { - let mut parts = s.split('.'); - let mut next = |kind| { - let Some(part) = parts.next().filter(|s| !s.is_empty()) else { - bail!("version number is missing {kind} version"); - }; - part.parse::() - .map_err(|_| eco_format!("`{part}` is not a valid {kind} version")) - }; - - let major = next("major")?; - let minor = next("minor")?; - let patch = next("patch")?; - if let Some(rest) = parts.next() { - bail!("version number has unexpected fourth component: `{rest}`"); - } - - Ok(Self { major, minor, patch }) - } -} - -impl Display for Version { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.patch) - } -} - -impl Serialize for Version { - fn serialize(&self, s: S) -> Result { - s.collect_str(self) - } -} - -impl<'de> Deserialize<'de> for Version { - fn deserialize>(d: D) -> Result { - let string = EcoString::deserialize(d)?; - string.parse().map_err(serde::de::Error::custom) - } -} - -/// A parsed package manifest. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct PackageManifest { - /// Details about the package itself. - pub package: PackageInfo, -} - -impl PackageManifest { - /// Parse the manifest from raw bytes. - pub fn parse(bytes: &[u8]) -> StrResult { - let string = std::str::from_utf8(bytes).map_err(FileError::from)?; - toml::from_str(string).map_err(|err| { - eco_format!("package manifest is malformed: {}", err.message()) - }) - } - - /// Ensure that this manifest is indeed for the specified package. - pub fn validate(&self, spec: &PackageSpec) -> StrResult<()> { - if self.package.name != spec.name { - bail!("package manifest contains mismatched name `{}`", self.package.name); - } - - if self.package.version != spec.version { - bail!( - "package manifest contains mismatched version {}", - self.package.version - ); - } - - Ok(()) - } -} - -/// The `package` key in the manifest. -/// -/// More fields are specified, but they are not relevant to the compiler. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct PackageInfo { - /// The name of the package within its namespace. - pub name: EcoString, - /// The package's version. - pub version: Version, - /// The path of the entrypoint into the package. - pub entrypoint: EcoString, -} diff --git a/src/font/book.rs b/src/font/book.rs deleted file mode 100644 index 2b7742bf..00000000 --- a/src/font/book.rs +++ /dev/null @@ -1,546 +0,0 @@ -use std::cmp::Reverse; -use std::collections::BTreeMap; - -use serde::{Deserialize, Serialize}; -use ttf_parser::{name_id, PlatformId, Tag}; -use unicode_segmentation::UnicodeSegmentation; - -use super::{Font, FontStretch, FontStyle, FontVariant, FontWeight}; - -/// Metadata about a collection of fonts. -#[derive(Default, Clone, Hash)] -pub struct FontBook { - /// Maps from lowercased family names to font indices. - families: BTreeMap>, - /// Metadata about each font in the collection. - infos: Vec, -} - -impl FontBook { - /// Create a new, empty font book. - pub fn new() -> Self { - Self { families: BTreeMap::new(), infos: vec![] } - } - - /// Create a font book for a collection of fonts. - pub fn from_fonts<'a>(fonts: impl IntoIterator) -> Self { - let mut book = Self::new(); - for font in fonts { - book.push(font.info().clone()); - } - book - } - - /// Insert metadata into the font book. - pub fn push(&mut self, info: FontInfo) { - let index = self.infos.len(); - let family = info.family.to_lowercase(); - self.families.entry(family).or_default().push(index); - self.infos.push(info); - } - - /// Get the font info for the given index. - pub fn info(&self, index: usize) -> Option<&FontInfo> { - self.infos.get(index) - } - - /// An ordered iterator over all font families this book knows and details - /// about the fonts that are part of them. - pub fn families( - &self, - ) -> impl Iterator)> + '_ { - // Since the keys are lowercased, we instead use the family field of the - // first face's info. - self.families.values().map(|ids| { - let family = self.infos[ids[0]].family.as_str(); - let infos = ids.iter().map(|&id| &self.infos[id]); - (family, infos) - }) - } - - /// Try to find a font from the given `family` that matches the given - /// `variant` as closely as possible. - /// - /// The `family` should be all lowercase. - pub fn select(&self, family: &str, variant: FontVariant) -> Option { - let ids = self.families.get(family)?; - self.find_best_variant(None, variant, ids.iter().copied()) - } - - /// Iterate over all variants of a family. - pub fn select_family(&self, family: &str) -> impl Iterator + '_ { - self.families - .get(family) - .map(|vec| vec.as_slice()) - .unwrap_or_default() - .iter() - .copied() - } - - /// Try to find and load a fallback font that - /// - is as close as possible to the font `like` (if any) - /// - is as close as possible to the given `variant` - /// - is suitable for shaping the given `text` - pub fn select_fallback( - &self, - like: Option<&FontInfo>, - variant: FontVariant, - text: &str, - ) -> Option { - // Find the fonts that contain the text's first char ... - let c = text.chars().next()?; - let ids = self - .infos - .iter() - .enumerate() - .filter(|(_, info)| info.coverage.contains(c as u32)) - .map(|(index, _)| index); - - // ... and find the best variant among them. - self.find_best_variant(like, variant, ids) - } - - /// Find the font in the passed iterator that - /// - is closest to the font `like` (if any) - /// - is closest to the given `variant` - /// - /// To do that we compute a key for all variants and select the one with the - /// minimal key. This key prioritizes: - /// - If `like` is some other font: - /// - Are both fonts (not) monospaced? - /// - Do both fonts (not) have serifs? - /// - How many words do the families share in their prefix? E.g. "Noto - /// Sans" and "Noto Sans Arabic" share two words, whereas "IBM Plex - /// Arabic" shares none with "Noto Sans", so prefer "Noto Sans Arabic" - /// if `like` is "Noto Sans". In case there are two equally good - /// matches, we prefer the shorter one because it is less special (e.g. - /// if `like` is "Noto Sans Arabic", we prefer "Noto Sans" over "Noto - /// Sans CJK HK".) - /// - The style (normal / italic / oblique). If we want italic or oblique - /// but it doesn't exist, the other one of the two is still better than - /// normal. - /// - The absolute distance to the target stretch. - /// - The absolute distance to the target weight. - fn find_best_variant( - &self, - like: Option<&FontInfo>, - variant: FontVariant, - ids: impl IntoIterator, - ) -> Option { - let mut best = None; - let mut best_key = None; - - for id in ids { - let current = &self.infos[id]; - let key = ( - like.map(|like| { - ( - current.flags.contains(FontFlags::MONOSPACE) - != like.flags.contains(FontFlags::MONOSPACE), - current.flags.contains(FontFlags::SERIF) - != like.flags.contains(FontFlags::SERIF), - Reverse(shared_prefix_words(¤t.family, &like.family)), - current.family.len(), - ) - }), - current.variant.style.distance(variant.style), - current.variant.stretch.distance(variant.stretch), - current.variant.weight.distance(variant.weight), - ); - - if best_key.map_or(true, |b| key < b) { - best = Some(id); - best_key = Some(key); - } - } - - best - } -} - -/// Properties of a single font. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct FontInfo { - /// The typographic font family this font is part of. - pub family: String, - /// Properties that distinguish this font from other fonts in the same - /// family. - pub variant: FontVariant, - /// Properties of the font. - pub flags: FontFlags, - /// The unicode coverage of the font. - pub coverage: Coverage, -} - -bitflags::bitflags! { - /// Bitflags describing characteristics of a font. - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] - #[derive(Serialize, Deserialize)] - #[serde(transparent)] - pub struct FontFlags: u32 { - /// All glyphs have the same width. - const MONOSPACE = 1 << 0; - /// Glyphs have short strokes at their stems. - const SERIF = 1 << 1; - } -} - -impl FontInfo { - /// Compute metadata for all fonts in the given data. - pub fn iter(data: &[u8]) -> impl Iterator + '_ { - let count = ttf_parser::fonts_in_collection(data).unwrap_or(1); - (0..count).filter_map(move |index| { - let ttf = ttf_parser::Face::parse(data, index).ok()?; - Self::from_ttf(&ttf) - }) - } - - /// Compute metadata for a single ttf-parser face. - pub(super) fn from_ttf(ttf: &ttf_parser::Face) -> Option { - // We cannot use Name ID 16 "Typographic Family", because for some - // fonts it groups together more than just Style / Weight / Stretch - // variants (e.g. Display variants of Noto fonts) and then some - // variants become inaccessible from Typst. And even though the - // fsSelection bit WWS should help us decide whether that is the - // case, it's wrong for some fonts (e.g. for certain variants of "Noto - // Sans Display"). - // - // So, instead we use Name ID 1 "Family" and trim many common - // suffixes for which know that they just describe styling (e.g. - // "ExtraBold"). - // - // Also, for Noto fonts we use Name ID 4 "Full Name" instead, - // because Name ID 1 "Family" sometimes contains "Display" and - // sometimes doesn't for the Display variants and that mixes things - // up. - let family = { - let mut family = find_name(ttf, name_id::FAMILY)?; - if family.starts_with("Noto") - || family.starts_with("NewCM") - || family.starts_with("NewComputerModern") - { - family = find_name(ttf, name_id::FULL_NAME)?; - } - typographic_family(&family).to_string() - }; - - let variant = { - let mut full = find_name(ttf, name_id::FULL_NAME).unwrap_or_default(); - full.make_ascii_lowercase(); - - // Some fonts miss the relevant bits for italic or oblique, so - // we also try to infer that from the full name. - let italic = ttf.is_italic() || full.contains("italic"); - let oblique = - ttf.is_oblique() || full.contains("oblique") || full.contains("slanted"); - - let style = match (italic, oblique) { - (false, false) => FontStyle::Normal, - (true, _) => FontStyle::Italic, - (_, true) => FontStyle::Oblique, - }; - - let weight = { - let mut number = ttf.weight().to_number(); - if (family.starts_with("NewCM") - || family.starts_with("New Computer Modern")) - && full.contains("book") - { - number += 50; - } - FontWeight::from_number(number) - }; - - let stretch = FontStretch::from_number(ttf.width().to_number()); - FontVariant { style, weight, stretch } - }; - - // Determine the unicode coverage. - let mut codepoints = vec![]; - for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) { - if subtable.is_unicode() { - subtable.codepoints(|c| codepoints.push(c)); - } - } - - let mut flags = FontFlags::empty(); - flags.set(FontFlags::MONOSPACE, ttf.is_monospaced()); - - // Determine whether this is a serif or sans-serif font. - if let Some(panose) = ttf - .raw_face() - .table(Tag::from_bytes(b"OS/2")) - .and_then(|os2| os2.get(32..45)) - { - if matches!(panose, [2, 2..=10, ..]) { - flags.insert(FontFlags::SERIF); - } - } - - Some(FontInfo { - family, - variant, - flags, - coverage: Coverage::from_vec(codepoints), - }) - } -} - -/// Try to find and decode the name with the given id. -pub(super) fn find_name(ttf: &ttf_parser::Face, name_id: u16) -> Option { - ttf.names().into_iter().find_map(|entry| { - if entry.name_id == name_id { - if let Some(string) = entry.to_string() { - return Some(string); - } - - if entry.platform_id == PlatformId::Macintosh && entry.encoding_id == 0 { - return Some(decode_mac_roman(entry.name)); - } - } - - None - }) -} - -/// Decode mac roman encoded bytes into a string. -fn decode_mac_roman(coded: &[u8]) -> String { - #[rustfmt::skip] - const TABLE: [char; 128] = [ - 'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è', - 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü', - '†', '°', '¢', '£', '§', '•', '¶', 'ß', '®', '©', '™', '´', '¨', '≠', 'Æ', 'Ø', - '∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑', '∏', 'π', '∫', 'ª', 'º', 'Ω', 'æ', 'ø', - '¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«', '»', '…', '\u{a0}', 'À', 'Ã', 'Õ', 'Œ', 'œ', - '–', '—', '“', '”', '‘', '’', '÷', '◊', 'ÿ', 'Ÿ', '⁄', '€', '‹', '›', 'fi', 'fl', - '‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á', 'Ë', 'È', 'Í', 'Î', 'Ï', 'Ì', 'Ó', 'Ô', - '\u{f8ff}', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜', '¯', '˘', '˙', '˚', '¸', '˝', '˛', 'ˇ', - ]; - - fn char_from_mac_roman(code: u8) -> char { - if code < 128 { - code as char - } else { - TABLE[(code - 128) as usize] - } - } - - coded.iter().copied().map(char_from_mac_roman).collect() -} - -/// Trim style naming from a family name and fix bad names. -fn typographic_family(mut family: &str) -> &str { - // Separators between names, modifiers and styles. - const SEPARATORS: [char; 3] = [' ', '-', '_']; - - // Modifiers that can appear in combination with suffixes. - const MODIFIERS: &[&str] = - &["extra", "ext", "ex", "x", "semi", "sem", "sm", "demi", "dem", "ultra"]; - - // Style suffixes. - #[rustfmt::skip] - const SUFFIXES: &[&str] = &[ - "normal", "italic", "oblique", "slanted", - "thin", "th", "hairline", "light", "lt", "regular", "medium", "med", - "md", "bold", "bd", "demi", "extb", "black", "blk", "bk", "heavy", - "narrow", "condensed", "cond", "cn", "cd", "compressed", "expanded", "exp" - ]; - - let mut extra = [].as_slice(); - let newcm = family.starts_with("NewCM") || family.starts_with("NewComputerModern"); - if newcm { - extra = &["book"]; - } - - // Trim spacing and weird leading dots in Apple fonts. - family = family.trim().trim_start_matches('.'); - - // Lowercase the string so that the suffixes match case-insensitively. - let lower = family.to_ascii_lowercase(); - let mut len = usize::MAX; - let mut trimmed = lower.as_str(); - - // Trim style suffixes repeatedly. - while trimmed.len() < len { - len = trimmed.len(); - - // Find style suffix. - let mut t = trimmed; - let mut shortened = false; - while let Some(s) = SUFFIXES.iter().chain(extra).find_map(|s| t.strip_suffix(s)) { - shortened = true; - t = s; - } - - if !shortened { - break; - } - - // Strip optional separator. - if let Some(s) = t.strip_suffix(SEPARATORS) { - trimmed = s; - t = s; - } - - // Also allow an extra modifier, but apply it only if it is separated it - // from the text before it (to prevent false positives). - if let Some(t) = MODIFIERS.iter().find_map(|s| t.strip_suffix(s)) { - if let Some(stripped) = t.strip_suffix(SEPARATORS) { - trimmed = stripped; - } - } - } - - // Apply style suffix trimming. - family = &family[..len]; - - if newcm { - family = family.trim_end_matches("10"); - } - - // Fix bad names. - match family { - "Noto Sans Symbols2" => "Noto Sans Symbols 2", - "NewComputerModern" => "New Computer Modern", - "NewComputerModernMono" => "New Computer Modern Mono", - "NewComputerModernSans" => "New Computer Modern Sans", - "NewComputerModernMath" => "New Computer Modern Math", - "NewCMUncial" | "NewComputerModernUncial" => "New Computer Modern Uncial", - other => other, - } -} - -/// How many words the two strings share in their prefix. -fn shared_prefix_words(left: &str, right: &str) -> usize { - left.unicode_words() - .zip(right.unicode_words()) - .take_while(|(l, r)| l == r) - .count() -} - -/// A compactly encoded set of codepoints. -/// -/// The set is represented by alternating specifications of how many codepoints -/// are not in the set and how many are in the set. -/// -/// For example, for the set `{2, 3, 4, 9, 10, 11, 15, 18, 19}`, there are: -/// - 2 codepoints not inside (0, 1) -/// - 3 codepoints inside (2, 3, 4) -/// - 4 codepoints not inside (5, 6, 7, 8) -/// - 3 codepoints inside (9, 10, 11) -/// - 3 codepoints not inside (12, 13, 14) -/// - 1 codepoint inside (15) -/// - 2 codepoints not inside (16, 17) -/// - 2 codepoints inside (18, 19) -/// -/// So the resulting encoding is `[2, 3, 4, 3, 3, 1, 2, 2]`. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -#[serde(transparent)] -pub struct Coverage(Vec); - -impl Coverage { - /// Encode a vector of codepoints. - pub fn from_vec(mut codepoints: Vec) -> Self { - codepoints.sort(); - codepoints.dedup(); - - let mut runs = Vec::new(); - let mut next = 0; - - for c in codepoints { - if let Some(run) = runs.last_mut().filter(|_| c == next) { - *run += 1; - } else { - runs.push(c - next); - runs.push(1); - } - - next = c + 1; - } - - Self(runs) - } - - /// Whether the codepoint is covered. - pub fn contains(&self, c: u32) -> bool { - let mut inside = false; - let mut cursor = 0; - - for &run in &self.0 { - if (cursor..cursor + run).contains(&c) { - return inside; - } - cursor += run; - inside = !inside; - } - - false - } - - /// Iterate over all covered codepoints. - pub fn iter(&self) -> impl Iterator + '_ { - let mut inside = false; - let mut cursor = 0; - self.0.iter().flat_map(move |run| { - let range = if inside { cursor..cursor + run } else { 0..0 }; - inside = !inside; - cursor += run; - range - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_trim_styles() { - assert_eq!(typographic_family("Atma Light"), "Atma"); - assert_eq!(typographic_family("eras bold"), "eras"); - assert_eq!(typographic_family("footlight mt light"), "footlight mt"); - assert_eq!(typographic_family("times new roman"), "times new roman"); - assert_eq!(typographic_family("noto sans mono cond sembd"), "noto sans mono"); - assert_eq!(typographic_family("noto serif SEMCOND sembd"), "noto serif"); - assert_eq!(typographic_family("crimson text"), "crimson text"); - assert_eq!(typographic_family("footlight light"), "footlight"); - assert_eq!(typographic_family("Noto Sans"), "Noto Sans"); - assert_eq!(typographic_family("Noto Sans Light"), "Noto Sans"); - assert_eq!(typographic_family("Noto Sans Semicondensed Heavy"), "Noto Sans"); - assert_eq!(typographic_family("Familx"), "Familx"); - assert_eq!(typographic_family("Font Ultra"), "Font Ultra"); - assert_eq!(typographic_family("Font Ultra Bold"), "Font"); - } - - #[test] - fn test_coverage() { - #[track_caller] - fn test(set: &[u32], runs: &[u32]) { - let coverage = Coverage::from_vec(set.to_vec()); - assert_eq!(coverage.0, runs); - - let max = 5 + set.iter().copied().max().unwrap_or_default(); - for c in 0..max { - assert_eq!(set.contains(&c), coverage.contains(c)); - } - } - - test(&[], &[]); - test(&[0], &[0, 1]); - test(&[1], &[1, 1]); - test(&[0, 1], &[0, 2]); - test(&[0, 1, 3], &[0, 2, 1, 1]); - test( - // {2, 3, 4, 9, 10, 11, 15, 18, 19} - &[18, 19, 2, 4, 9, 11, 15, 3, 3, 10], - &[2, 3, 4, 3, 3, 1, 2, 2], - ) - } - - #[test] - fn test_coverage_iter() { - let codepoints = vec![2, 3, 7, 8, 9, 14, 15, 19, 21]; - let coverage = Coverage::from_vec(codepoints.clone()); - assert_eq!(coverage.iter().collect::>(), codepoints); - } -} diff --git a/src/font/mod.rs b/src/font/mod.rs deleted file mode 100644 index 2353e51c..00000000 --- a/src/font/mod.rs +++ /dev/null @@ -1,249 +0,0 @@ -//! Font handling. - -mod book; -mod variant; - -pub use self::book::{Coverage, FontBook, FontFlags, FontInfo}; -pub use self::variant::{FontStretch, FontStyle, FontVariant, FontWeight}; - -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::sync::Arc; - -use ttf_parser::GlyphId; - -use self::book::find_name; -use crate::eval::Cast; -use crate::geom::Em; -use crate::util::Bytes; - -/// An OpenType font. -/// -/// Values of this type are cheap to clone and hash. -#[derive(Clone)] -pub struct Font(Arc); - -/// The internal representation of a font. -struct Repr { - /// The raw font data, possibly shared with other fonts from the same - /// collection. The vector's allocation must not move, because `ttf` points - /// into it using unsafe code. - data: Bytes, - /// The font's index in the buffer. - index: u32, - /// Metadata about the font. - info: FontInfo, - /// The font's metrics. - metrics: FontMetrics, - /// The underlying ttf-parser face. - ttf: ttf_parser::Face<'static>, - /// The underlying rustybuzz face. - rusty: rustybuzz::Face<'static>, -} - -impl Font { - /// Parse a font from data and collection index. - pub fn new(data: Bytes, index: u32) -> Option { - // Safety: - // - The slices's location is stable in memory: - // - We don't move the underlying vector - // - Nobody else can move it since we have a strong ref to the `Arc`. - // - The internal 'static lifetime is not leaked because its rewritten - // to the self-lifetime in `ttf()`. - let slice: &'static [u8] = - unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) }; - - let ttf = ttf_parser::Face::parse(slice, index).ok()?; - let rusty = rustybuzz::Face::from_slice(slice, index)?; - let metrics = FontMetrics::from_ttf(&ttf); - let info = FontInfo::from_ttf(&ttf)?; - - Some(Self(Arc::new(Repr { data, index, info, metrics, ttf, rusty }))) - } - - /// Parse all fonts in the given data. - pub fn iter(data: Bytes) -> impl Iterator { - let count = ttf_parser::fonts_in_collection(&data).unwrap_or(1); - (0..count).filter_map(move |index| Self::new(data.clone(), index)) - } - - /// The underlying buffer. - pub fn data(&self) -> &Bytes { - &self.0.data - } - - /// The font's index in the buffer. - pub fn index(&self) -> u32 { - self.0.index - } - - /// The font's metadata. - pub fn info(&self) -> &FontInfo { - &self.0.info - } - - /// The font's metrics. - pub fn metrics(&self) -> &FontMetrics { - &self.0.metrics - } - - /// The number of font units per one em. - pub fn units_per_em(&self) -> f64 { - self.0.metrics.units_per_em - } - - /// Convert from font units to an em length. - pub fn to_em(&self, units: impl Into) -> Em { - Em::from_units(units, self.units_per_em()) - } - - /// Look up the horizontal advance width of a glyph. - pub fn advance(&self, glyph: u16) -> Option { - self.0 - .ttf - .glyph_hor_advance(GlyphId(glyph)) - .map(|units| self.to_em(units)) - } - - /// Lookup a name by id. - pub fn find_name(&self, id: u16) -> Option { - find_name(&self.0.ttf, id) - } - - /// A reference to the underlying `ttf-parser` face. - pub fn ttf(&self) -> &ttf_parser::Face<'_> { - // We can't implement Deref because that would leak the - // internal 'static lifetime. - &self.0.ttf - } - - /// A reference to the underlying `rustybuzz` face. - pub fn rusty(&self) -> &rustybuzz::Face<'_> { - // We can't implement Deref because that would leak the - // internal 'static lifetime. - &self.0.rusty - } -} - -impl Hash for Font { - fn hash(&self, state: &mut H) { - self.0.data.hash(state); - self.0.index.hash(state); - } -} - -impl Debug for Font { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Font({})", self.info().family) - } -} - -impl Eq for Font {} - -impl PartialEq for Font { - fn eq(&self, other: &Self) -> bool { - self.0.data == other.0.data && self.0.index == other.0.index - } -} - -/// Metrics of a font. -#[derive(Debug, Copy, Clone)] -pub struct FontMetrics { - /// How many font units represent one em unit. - pub units_per_em: f64, - /// The distance from the baseline to the typographic ascender. - pub ascender: Em, - /// The approximate height of uppercase letters. - pub cap_height: Em, - /// The approximate height of non-ascending lowercase letters. - pub x_height: Em, - /// The distance from the baseline to the typographic descender. - pub descender: Em, - /// Recommended metrics for a strikethrough line. - pub strikethrough: LineMetrics, - /// Recommended metrics for an underline. - pub underline: LineMetrics, - /// Recommended metrics for an overline. - pub overline: LineMetrics, -} - -impl FontMetrics { - /// Extract the font's metrics. - pub fn from_ttf(ttf: &ttf_parser::Face) -> Self { - let units_per_em = f64::from(ttf.units_per_em()); - let to_em = |units| Em::from_units(units, units_per_em); - - let ascender = to_em(ttf.typographic_ascender().unwrap_or(ttf.ascender())); - let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em); - let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em); - let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender())); - let strikeout = ttf.strikeout_metrics(); - let underline = ttf.underline_metrics(); - - let strikethrough = LineMetrics { - position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)), - thickness: strikeout - .or(underline) - .map_or(Em::new(0.06), |s| to_em(s.thickness)), - }; - - let underline = LineMetrics { - position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)), - thickness: underline - .or(strikeout) - .map_or(Em::new(0.06), |s| to_em(s.thickness)), - }; - - let overline = LineMetrics { - position: cap_height + Em::new(0.1), - thickness: underline.thickness, - }; - - Self { - units_per_em, - ascender, - cap_height, - x_height, - descender, - strikethrough, - underline, - overline, - } - } - - /// Look up a vertical metric. - pub fn vertical(&self, metric: VerticalFontMetric) -> Em { - match metric { - VerticalFontMetric::Ascender => self.ascender, - VerticalFontMetric::CapHeight => self.cap_height, - VerticalFontMetric::XHeight => self.x_height, - VerticalFontMetric::Baseline => Em::zero(), - VerticalFontMetric::Descender => self.descender, - } - } -} - -/// Metrics for a decorative line. -#[derive(Debug, Copy, Clone)] -pub struct LineMetrics { - /// The vertical offset of the line from the baseline. Positive goes - /// upwards, negative downwards. - pub position: Em, - /// The thickness of the line. - pub thickness: Em, -} - -/// Identifies a vertical metric of a font. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum VerticalFontMetric { - /// The font's ascender, which typically exceeds the height of all glyphs. - Ascender, - /// The approximate height of uppercase letters. - CapHeight, - /// The approximate height of non-ascending lowercase letters. - XHeight, - /// The baseline on which the letters rest. - Baseline, - /// The font's ascender, which typically exceeds the depth of all glyphs. - Descender, -} diff --git a/src/font/variant.rs b/src/font/variant.rs deleted file mode 100644 index d4508a59..00000000 --- a/src/font/variant.rs +++ /dev/null @@ -1,270 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use serde::{Deserialize, Serialize}; - -use crate::eval::{cast, Cast, IntoValue}; -use crate::geom::Ratio; - -/// Properties that distinguish a font from other fonts in the same family. -#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] -pub struct FontVariant { - /// The style of the font (normal / italic / oblique). - pub style: FontStyle, - /// How heavy the font is (100 - 900). - pub weight: FontWeight, - /// How condensed or expanded the font is (0.5 - 2.0). - pub stretch: FontStretch, -} - -impl FontVariant { - /// Create a variant from its three components. - pub fn new(style: FontStyle, weight: FontWeight, stretch: FontStretch) -> Self { - Self { style, weight, stretch } - } -} - -impl Debug for FontVariant { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{:?}-{:?}-{:?}", self.style, self.weight, self.stretch) - } -} - -/// The style of a font. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize, Cast)] -#[serde(rename_all = "kebab-case")] -pub enum FontStyle { - /// The default, typically upright style. - Normal, - /// A cursive style with custom letterform. - Italic, - /// Just a slanted version of the normal style. - Oblique, -} - -impl FontStyle { - /// The conceptual distance between the styles, expressed as a number. - pub fn distance(self, other: Self) -> u16 { - if self == other { - 0 - } else if self != Self::Normal && other != Self::Normal { - 1 - } else { - 2 - } - } -} - -impl Default for FontStyle { - fn default() -> Self { - Self::Normal - } -} - -/// The weight of a font. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] -#[serde(transparent)] -pub struct FontWeight(u16); - -impl FontWeight { - /// Thin weight (100). - pub const THIN: Self = Self(100); - - /// Extra light weight (200). - pub const EXTRALIGHT: Self = Self(200); - - /// Light weight (300). - pub const LIGHT: Self = Self(300); - - /// Regular weight (400). - pub const REGULAR: Self = Self(400); - - /// Medium weight (500). - pub const MEDIUM: Self = Self(500); - - /// Semibold weight (600). - pub const SEMIBOLD: Self = Self(600); - - /// Bold weight (700). - pub const BOLD: Self = Self(700); - - /// Extrabold weight (800). - pub const EXTRABOLD: Self = Self(800); - - /// Black weight (900). - pub const BLACK: Self = Self(900); - - /// Create a font weight from a number between 100 and 900, clamping it if - /// necessary. - pub fn from_number(weight: u16) -> Self { - Self(weight.max(100).min(900)) - } - - /// The number between 100 and 900. - pub fn to_number(self) -> u16 { - self.0 - } - - /// Add (or remove) weight, saturating at the boundaries of 100 and 900. - pub fn thicken(self, delta: i16) -> Self { - Self((self.0 as i16).saturating_add(delta).max(100).min(900) as u16) - } - - /// The absolute number distance between this and another font weight. - pub fn distance(self, other: Self) -> u16 { - (self.0 as i16 - other.0 as i16).unsigned_abs() - } -} - -impl Default for FontWeight { - fn default() -> Self { - Self::REGULAR - } -} - -impl Debug for FontWeight { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -cast! { - FontWeight, - self => IntoValue::into_value(match self { - FontWeight::THIN => "thin", - FontWeight::EXTRALIGHT => "extralight", - FontWeight::LIGHT => "light", - FontWeight::REGULAR => "regular", - FontWeight::MEDIUM => "medium", - FontWeight::SEMIBOLD => "semibold", - FontWeight::BOLD => "bold", - FontWeight::EXTRABOLD => "extrabold", - FontWeight::BLACK => "black", - _ => return self.to_number().into_value(), - }), - v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), - /// Thin weight (100). - "thin" => Self::THIN, - /// Extra light weight (200). - "extralight" => Self::EXTRALIGHT, - /// Light weight (300). - "light" => Self::LIGHT, - /// Regular weight (400). - "regular" => Self::REGULAR, - /// Medium weight (500). - "medium" => Self::MEDIUM, - /// Semibold weight (600). - "semibold" => Self::SEMIBOLD, - /// Bold weight (700). - "bold" => Self::BOLD, - /// Extrabold weight (800). - "extrabold" => Self::EXTRABOLD, - /// Black weight (900). - "black" => Self::BLACK, -} - -/// The width of a font. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[derive(Serialize, Deserialize)] -#[serde(transparent)] -pub struct FontStretch(u16); - -impl FontStretch { - /// Ultra-condensed stretch (50%). - pub const ULTRA_CONDENSED: Self = Self(500); - - /// Extra-condensed stretch weight (62.5%). - pub const EXTRA_CONDENSED: Self = Self(625); - - /// Condensed stretch (75%). - pub const CONDENSED: Self = Self(750); - - /// Semi-condensed stretch (87.5%). - pub const SEMI_CONDENSED: Self = Self(875); - - /// Normal stretch (100%). - pub const NORMAL: Self = Self(1000); - - /// Semi-expanded stretch (112.5%). - pub const SEMI_EXPANDED: Self = Self(1125); - - /// Expanded stretch (125%). - pub const EXPANDED: Self = Self(1250); - - /// Extra-expanded stretch (150%). - pub const EXTRA_EXPANDED: Self = Self(1500); - - /// Ultra-expanded stretch (200%). - pub const ULTRA_EXPANDED: Self = Self(2000); - - /// Create a font stretch from a ratio between 0.5 and 2.0, clamping it if - /// necessary. - pub fn from_ratio(ratio: Ratio) -> Self { - Self((ratio.get().max(0.5).min(2.0) * 1000.0) as u16) - } - - /// Create a font stretch from an OpenType-style number between 1 and 9, - /// clamping it if necessary. - pub fn from_number(stretch: u16) -> Self { - match stretch { - 0 | 1 => Self::ULTRA_CONDENSED, - 2 => Self::EXTRA_CONDENSED, - 3 => Self::CONDENSED, - 4 => Self::SEMI_CONDENSED, - 5 => Self::NORMAL, - 6 => Self::SEMI_EXPANDED, - 7 => Self::EXPANDED, - 8 => Self::EXTRA_EXPANDED, - _ => Self::ULTRA_EXPANDED, - } - } - - /// The ratio between 0.5 and 2.0 corresponding to this stretch. - pub fn to_ratio(self) -> Ratio { - Ratio::new(self.0 as f64 / 1000.0) - } - - /// The absolute ratio distance between this and another font stretch. - pub fn distance(self, other: Self) -> Ratio { - (self.to_ratio() - other.to_ratio()).abs() - } -} - -impl Default for FontStretch { - fn default() -> Self { - Self::NORMAL - } -} - -impl Debug for FontStretch { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.to_ratio().fmt(f) - } -} - -cast! { - FontStretch, - self => self.to_ratio().into_value(), - v: Ratio => Self::from_ratio(v), -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_font_weight_distance() { - let d = |a, b| FontWeight(a).distance(FontWeight(b)); - assert_eq!(d(500, 200), 300); - assert_eq!(d(500, 500), 0); - assert_eq!(d(500, 900), 400); - assert_eq!(d(10, 100), 90); - } - - #[test] - fn test_font_stretch_debug() { - assert_eq!(format!("{:?}", FontStretch::EXPANDED), "125%") - } -} diff --git a/src/geom/abs.rs b/src/geom/abs.rs deleted file mode 100644 index 4ca3a9a1..00000000 --- a/src/geom/abs.rs +++ /dev/null @@ -1,266 +0,0 @@ -use super::*; - -/// An absolute length. -#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Abs(Scalar); - -impl Abs { - /// The zero length. - pub const fn zero() -> Self { - Self(Scalar(0.0)) - } - - /// The infinite length. - pub const fn inf() -> Self { - Self(Scalar(f64::INFINITY)) - } - - /// Create an absolute length from a number of raw units. - pub const fn raw(raw: f64) -> Self { - Self(Scalar(raw)) - } - - /// Create an absolute length from a value in a unit. - pub fn with_unit(val: f64, unit: AbsUnit) -> Self { - Self(Scalar(val * unit.raw_scale())) - } - - /// Create an absolute length from a number of points. - pub fn pt(pt: f64) -> Self { - Self::with_unit(pt, AbsUnit::Pt) - } - - /// Create an absolute length from a number of millimeters. - pub fn mm(mm: f64) -> Self { - Self::with_unit(mm, AbsUnit::Mm) - } - - /// Create an absolute length from a number of centimeters. - pub fn cm(cm: f64) -> Self { - Self::with_unit(cm, AbsUnit::Cm) - } - - /// Create an absolute length from a number of inches. - pub fn inches(inches: f64) -> Self { - Self::with_unit(inches, AbsUnit::In) - } - - /// Get the value of this absolute length in raw units. - pub const fn to_raw(self) -> f64 { - (self.0).0 - } - - /// Get the value of this absolute length in a unit. - pub fn to_unit(self, unit: AbsUnit) -> f64 { - self.to_raw() / unit.raw_scale() - } - - /// Convert this to a number of points. - pub fn to_pt(self) -> f64 { - self.to_unit(AbsUnit::Pt) - } - - /// Convert this to a number of millimeters. - pub fn to_mm(self) -> f64 { - self.to_unit(AbsUnit::Mm) - } - - /// Convert this to a number of centimeters. - pub fn to_cm(self) -> f64 { - self.to_unit(AbsUnit::Cm) - } - - /// Convert this to a number of inches. - pub fn to_inches(self) -> f64 { - self.to_unit(AbsUnit::In) - } - - /// The absolute value of this length. - pub fn abs(self) -> Self { - Self::raw(self.to_raw().abs()) - } - - /// The minimum of this and another absolute length. - pub fn min(self, other: Self) -> Self { - Self(self.0.min(other.0)) - } - - /// Set to the minimum of this and another absolute length. - pub fn set_min(&mut self, other: Self) { - *self = (*self).min(other); - } - - /// The maximum of this and another absolute length. - pub fn max(self, other: Self) -> Self { - Self(self.0.max(other.0)) - } - - /// Set to the maximum of this and another absolute length. - pub fn set_max(&mut self, other: Self) { - *self = (*self).max(other); - } - - /// Whether the other absolute length fits into this one (i.e. is smaller). - /// Allows for a bit of slack. - pub fn fits(self, other: Self) -> bool { - self.0 + 1e-6 >= other.0 - } - - /// Compares two absolute lengths for whether they are approximately equal. - pub fn approx_eq(self, other: Self) -> bool { - self == other || (self - other).to_raw().abs() < 1e-6 - } - - /// Perform a checked division by a number, returning zero if the result - /// is not finite. - pub fn safe_div(self, number: f64) -> Self { - let result = self.to_raw() / number; - if result.is_finite() { - Self::raw(result) - } else { - Self::zero() - } - } -} - -impl Numeric for Abs { - fn zero() -> Self { - Self::zero() - } - - fn is_finite(self) -> bool { - self.0.is_finite() - } -} - -impl Debug for Abs { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}pt", round_2(self.to_pt())) - } -} - -impl Neg for Abs { - type Output = Self; - - fn neg(self) -> Self { - Self(-self.0) - } -} - -impl Add for Abs { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self(self.0 + other.0) - } -} - -sub_impl!(Abs - Abs -> Abs); - -impl Mul for Abs { - type Output = Self; - - fn mul(self, other: f64) -> Self { - Self(self.0 * other) - } -} - -impl Mul for f64 { - type Output = Abs; - - fn mul(self, other: Abs) -> Abs { - other * self - } -} - -impl Div for Abs { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self(self.0 / other) - } -} - -impl Div for Abs { - type Output = f64; - - fn div(self, other: Self) -> f64 { - self.to_raw() / other.to_raw() - } -} - -assign_impl!(Abs += Abs); -assign_impl!(Abs -= Abs); -assign_impl!(Abs *= f64); -assign_impl!(Abs /= f64); - -impl Rem for Abs { - type Output = Self; - - fn rem(self, other: Self) -> Self::Output { - Self(self.0 % other.0) - } -} - -impl Sum for Abs { - fn sum>(iter: I) -> Self { - Self(iter.map(|s| s.0).sum()) - } -} - -impl<'a> Sum<&'a Self> for Abs { - fn sum>(iter: I) -> Self { - Self(iter.map(|s| s.0).sum()) - } -} - -cast! { - Abs, - self => Value::Length(self.into()), -} - -/// Different units of absolute measurement. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum AbsUnit { - /// Points. - Pt, - /// Millimeters. - Mm, - /// Centimeters. - Cm, - /// Inches. - In, -} - -impl AbsUnit { - /// How many raw units correspond to a value of `1.0` in this unit. - fn raw_scale(self) -> f64 { - match self { - AbsUnit::Pt => 1.0, - AbsUnit::Mm => 2.83465, - AbsUnit::Cm => 28.3465, - AbsUnit::In => 72.0, - } - } -} - -impl Debug for AbsUnit { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - AbsUnit::Mm => "mm", - AbsUnit::Pt => "pt", - AbsUnit::Cm => "cm", - AbsUnit::In => "in", - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_length_unit_conversion() { - assert!((Abs::mm(150.0).to_cm() - 15.0) < 1e-4); - } -} diff --git a/src/geom/align.rs b/src/geom/align.rs deleted file mode 100644 index 47acd3a6..00000000 --- a/src/geom/align.rs +++ /dev/null @@ -1,239 +0,0 @@ -use super::*; - -/// Where to align something along an axis. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum Align { - /// Align at the left side. - Left, - /// Align in the horizontal middle. - Center, - /// Align at the right side. - Right, - /// Align at the top side. - Top, - /// Align in the vertical middle. - Horizon, - /// Align at the bottom side. - Bottom, -} - -impl Align { - /// Top-left alignment. - pub const LEFT_TOP: Axes = Axes { x: Align::Left, y: Align::Top }; - - /// Center-horizon alignment. - pub const CENTER_HORIZON: Axes = Axes { x: Align::Center, y: Align::Horizon }; - - /// The axis this alignment belongs to. - pub const fn axis(self) -> Axis { - match self { - Self::Left | Self::Center | Self::Right => Axis::X, - Self::Top | Self::Horizon | Self::Bottom => Axis::Y, - } - } - - /// The inverse alignment. - pub const fn inv(self) -> Self { - match self { - Self::Left => Self::Right, - Self::Center => Self::Center, - Self::Right => Self::Left, - Self::Top => Self::Bottom, - Self::Horizon => Self::Horizon, - Self::Bottom => Self::Top, - } - } - - /// Returns the position of this alignment in a container with the given - /// extent. - pub fn position(self, extent: Abs) -> Abs { - match self { - Self::Left | Self::Top => Abs::zero(), - Self::Center | Self::Horizon => extent / 2.0, - Self::Right | Self::Bottom => extent, - } - } -} - -impl From for Align { - fn from(side: Side) -> Self { - match side { - Side::Left => Self::Left, - Side::Top => Self::Top, - Side::Right => Self::Right, - Side::Bottom => Self::Bottom, - } - } -} - -impl Debug for Align { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Left => "left", - Self::Center => "center", - Self::Right => "right", - Self::Top => "top", - Self::Horizon => "horizon", - Self::Bottom => "bottom", - }) - } -} - -/// The generic alignment representation. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum GenAlign { - /// Align at the start side of the text direction. - Start, - /// Align at the end side of the text direction. - End, - /// Align at a specific alignment. - Specific(Align), -} - -impl GenAlign { - /// The axis this alignment belongs to. - pub const fn axis(self) -> Axis { - match self { - Self::Start | Self::End => Axis::X, - Self::Specific(align) => align.axis(), - } - } -} - -impl From for GenAlign { - fn from(align: Align) -> Self { - Self::Specific(align) - } -} - -impl From for GenAlign { - fn from(align: HorizontalAlign) -> Self { - align.0 - } -} - -impl From for GenAlign { - fn from(align: VerticalAlign) -> Self { - align.0 - } -} - -impl Debug for GenAlign { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Start => f.pad("start"), - Self::End => f.pad("end"), - Self::Specific(align) => align.fmt(f), - } - } -} - -cast! { - type GenAlign: "alignment", -} - -cast! { - type Axes: "2d alignment", -} - -cast! { - Axes, - self => self.map(GenAlign::from).into_value(), -} - -cast! { - Axes>, - self => match (self.x, self.y) { - (Some(x), Some(y)) => Axes::new(x, y).into_value(), - (Some(x), None) => x.into_value(), - (None, Some(y)) => y.into_value(), - (None, None) => Value::None, - }, - align: GenAlign => { - let mut aligns = Axes::default(); - aligns.set(align.axis(), Some(align)); - aligns - }, - aligns: Axes => aligns.map(Some), -} - -impl From> for Axes> { - fn from(axes: Axes) -> Self { - axes.map(Some) - } -} - -impl From> for Axes> { - fn from(axes: Axes) -> Self { - axes.map(GenAlign::Specific).into() - } -} - -impl From for Axes> { - fn from(align: Align) -> Self { - let mut axes = Axes::splat(None); - axes.set(align.axis(), Some(align.into())); - axes - } -} - -impl Resolve for GenAlign { - type Output = Align; - - fn resolve(self, styles: StyleChain) -> Self::Output { - let dir = item!(dir)(styles); - match self { - Self::Start => dir.start().into(), - Self::End => dir.end().into(), - Self::Specific(align) => align, - } - } -} - -impl Fold for GenAlign { - type Output = Self; - - fn fold(self, _: Self::Output) -> Self::Output { - self - } -} - -impl Fold for Align { - type Output = Self; - - fn fold(self, _: Self::Output) -> Self::Output { - self - } -} - -/// Utility struct to restrict a passed alignment value to the horizontal axis -/// on cast. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct HorizontalAlign(pub GenAlign); - -cast! { - HorizontalAlign, - self => self.0.into_value(), - align: GenAlign => { - if align.axis() != Axis::X { - bail!("alignment must be horizontal"); - } - Self(align) - }, -} - -/// Utility struct to restrict a passed alignment value to the vertical axis on -/// cast. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct VerticalAlign(pub GenAlign); - -cast! { - VerticalAlign, - self => self.0.into_value(), - align: GenAlign => { - if align.axis() != Axis::Y { - bail!("alignment must be vertical"); - } - Self(align) - }, -} diff --git a/src/geom/angle.rs b/src/geom/angle.rs deleted file mode 100644 index c03810d9..00000000 --- a/src/geom/angle.rs +++ /dev/null @@ -1,188 +0,0 @@ -use super::*; - -/// An angle. -#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Angle(Scalar); - -impl Angle { - /// The zero angle. - pub const fn zero() -> Self { - Self(Scalar(0.0)) - } - - /// Create an angle from a number of raw units. - pub const fn raw(raw: f64) -> Self { - Self(Scalar(raw)) - } - - /// Create an angle from a value in a unit. - pub fn with_unit(val: f64, unit: AngleUnit) -> Self { - Self(Scalar(val * unit.raw_scale())) - } - - /// Create an angle from a number of radians. - pub fn rad(rad: f64) -> Self { - Self::with_unit(rad, AngleUnit::Rad) - } - - /// Create an angle from a number of degrees. - pub fn deg(deg: f64) -> Self { - Self::with_unit(deg, AngleUnit::Deg) - } - - /// Get the value of this angle in raw units. - pub const fn to_raw(self) -> f64 { - (self.0).0 - } - - /// Get the value of this angle in a unit. - pub fn to_unit(self, unit: AngleUnit) -> f64 { - self.to_raw() / unit.raw_scale() - } - - /// Convert this to a number of radians. - pub fn to_rad(self) -> f64 { - self.to_unit(AngleUnit::Rad) - } - - /// Convert this to a number of degrees. - pub fn to_deg(self) -> f64 { - self.to_unit(AngleUnit::Deg) - } - - /// The absolute value of the this angle. - pub fn abs(self) -> Self { - Self::raw(self.to_raw().abs()) - } - - /// Get the sine of this angle in radians. - pub fn sin(self) -> f64 { - self.to_rad().sin() - } - - /// Get the cosine of this angle in radians. - pub fn cos(self) -> f64 { - self.to_rad().cos() - } - - /// Get the tangent of this angle in radians. - pub fn tan(self) -> f64 { - self.to_rad().tan() - } -} - -impl Numeric for Angle { - fn zero() -> Self { - Self::zero() - } - - fn is_finite(self) -> bool { - self.0.is_finite() - } -} - -impl Debug for Angle { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}deg", round_2(self.to_deg())) - } -} - -impl Neg for Angle { - type Output = Self; - - fn neg(self) -> Self { - Self(-self.0) - } -} - -impl Add for Angle { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self(self.0 + other.0) - } -} - -sub_impl!(Angle - Angle -> Angle); - -impl Mul for Angle { - type Output = Self; - - fn mul(self, other: f64) -> Self { - Self(self.0 * other) - } -} - -impl Mul for f64 { - type Output = Angle; - - fn mul(self, other: Angle) -> Angle { - other * self - } -} - -impl Div for Angle { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self(self.0 / other) - } -} - -impl Div for Angle { - type Output = f64; - - fn div(self, other: Self) -> f64 { - self.to_raw() / other.to_raw() - } -} - -assign_impl!(Angle += Angle); -assign_impl!(Angle -= Angle); -assign_impl!(Angle *= f64); -assign_impl!(Angle /= f64); - -impl Sum for Angle { - fn sum>(iter: I) -> Self { - Self(iter.map(|s| s.0).sum()) - } -} - -/// Different units of angular measurement. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum AngleUnit { - /// Radians. - Rad, - /// Degrees. - Deg, -} - -impl AngleUnit { - /// How many raw units correspond to a value of `1.0` in this unit. - fn raw_scale(self) -> f64 { - match self { - Self::Rad => 1.0, - Self::Deg => PI / 180.0, - } - } -} - -impl Debug for AngleUnit { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Rad => "rad", - Self::Deg => "deg", - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_angle_unit_conversion() { - assert!((Angle::rad(2.0 * PI).to_deg() - 360.0) < 1e-4); - assert!((Angle::deg(45.0).to_rad() - std::f64::consts::FRAC_PI_4) < 1e-4); - } -} diff --git a/src/geom/axes.rs b/src/geom/axes.rs deleted file mode 100644 index 059d3bb2..00000000 --- a/src/geom/axes.rs +++ /dev/null @@ -1,305 +0,0 @@ -use std::any::Any; -use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; - -use super::*; - -/// A container with a horizontal and vertical component. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Axes { - /// The horizontal component. - pub x: T, - /// The vertical component. - pub y: T, -} - -impl Axes { - /// Create a new instance from the two components. - pub const fn new(x: T, y: T) -> Self { - Self { x, y } - } - - /// Create a new instance with two equal components. - pub fn splat(v: T) -> Self - where - T: Clone, - { - Self { x: v.clone(), y: v } - } - - /// Map the individual fields with `f`. - pub fn map(self, mut f: F) -> Axes - where - F: FnMut(T) -> U, - { - Axes { x: f(self.x), y: f(self.y) } - } - - /// Convert from `&Axes` to `Axes<&T>`. - pub fn as_ref(&self) -> Axes<&T> { - Axes { x: &self.x, y: &self.y } - } - - /// Convert from `&Axes` to `Axes<&::Target>`. - pub fn as_deref(&self) -> Axes<&T::Target> - where - T: Deref, - { - Axes { x: &self.x, y: &self.y } - } - - /// Convert from `&mut Axes` to `Axes<&mut T>`. - pub fn as_mut(&mut self) -> Axes<&mut T> { - Axes { x: &mut self.x, y: &mut self.y } - } - - /// Zip two instances into an instance over a tuple. - pub fn zip(self, other: Axes) -> Axes<(T, U)> { - Axes { x: (self.x, other.x), y: (self.y, other.y) } - } - - /// Whether a condition is true for at least one of fields. - pub fn any(self, mut f: F) -> bool - where - F: FnMut(&T) -> bool, - { - f(&self.x) || f(&self.y) - } - - /// Whether a condition is true for both fields. - pub fn all(self, mut f: F) -> bool - where - F: FnMut(&T) -> bool, - { - f(&self.x) && f(&self.y) - } - - /// Filter the individual fields with a mask. - pub fn filter(self, mask: Axes) -> Axes> { - Axes { - x: if mask.x { Some(self.x) } else { None }, - y: if mask.y { Some(self.y) } else { None }, - } - } -} - -impl Axes { - /// Create a new instance with y set to its default value. - pub fn with_x(x: T) -> Self { - Self { x, y: T::default() } - } - - /// Create a new instance with x set to its default value. - pub fn with_y(y: T) -> Self { - Self { x: T::default(), y } - } -} - -impl Axes { - /// The component-wise minimum of this and another instance. - pub fn min(self, other: Self) -> Self { - Self { x: self.x.min(other.x), y: self.y.min(other.y) } - } - - /// The component-wise minimum of this and another instance. - pub fn max(self, other: Self) -> Self { - Self { x: self.x.max(other.x), y: self.y.max(other.y) } - } - - /// The minimum of width and height. - pub fn min_by_side(self) -> T { - self.x.min(self.y) - } - - /// The minimum of width and height. - pub fn max_by_side(self) -> T { - self.x.max(self.y) - } -} - -impl Get for Axes { - type Component = T; - - fn get_ref(&self, axis: Axis) -> &T { - match axis { - Axis::X => &self.x, - Axis::Y => &self.y, - } - } - - fn get_mut(&mut self, axis: Axis) -> &mut T { - match axis { - Axis::X => &mut self.x, - Axis::Y => &mut self.y, - } - } -} - -impl Debug for Axes -where - T: Debug + 'static, -{ - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if let Axes { x: Some(x), y: Some(y) } = - self.as_ref().map(|v| (v as &dyn Any).downcast_ref::()) - { - write!(f, "{:?} + {:?}", x, y) - } else if (&self.x as &dyn Any).is::() { - write!(f, "Size({:?}, {:?})", self.x, self.y) - } else { - write!(f, "Axes({:?}, {:?})", self.x, self.y) - } - } -} - -/// The two layouting axes. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum Axis { - /// The horizontal axis. - X, - /// The vertical axis. - Y, -} - -impl Axis { - /// The direction with the given positivity for this axis. - pub fn dir(self, positive: bool) -> Dir { - match (self, positive) { - (Self::X, true) => Dir::LTR, - (Self::X, false) => Dir::RTL, - (Self::Y, true) => Dir::TTB, - (Self::Y, false) => Dir::BTT, - } - } - - /// The other axis. - pub fn other(self) -> Self { - match self { - Self::X => Self::Y, - Self::Y => Self::X, - } - } -} - -impl Debug for Axis { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::X => "horizontal", - Self::Y => "vertical", - }) - } -} - -impl Axes> { - /// Unwrap the individual fields. - pub fn unwrap_or(self, other: Axes) -> Axes { - Axes { - x: self.x.unwrap_or(other.x), - y: self.y.unwrap_or(other.y), - } - } -} - -impl Axes> { - /// Unwrap the individual fields. - pub fn unwrap_or(self, other: Axes) -> Axes { - Axes { - x: self.x.unwrap_or(other.x), - y: self.y.unwrap_or(other.y), - } - } -} - -impl Axes { - /// Select `t.x` if `self.x` is true and `f.x` otherwise and same for `y`. - pub fn select(self, t: Axes, f: Axes) -> Axes { - Axes { - x: if self.x { t.x } else { f.x }, - y: if self.y { t.y } else { f.y }, - } - } -} - -impl Not for Axes { - type Output = Self; - - fn not(self) -> Self::Output { - Self { x: !self.x, y: !self.y } - } -} - -impl BitOr for Axes { - type Output = Self; - - fn bitor(self, rhs: Self) -> Self::Output { - Self { x: self.x | rhs.x, y: self.y | rhs.y } - } -} - -impl BitOr for Axes { - type Output = Self; - - fn bitor(self, rhs: bool) -> Self::Output { - Self { x: self.x | rhs, y: self.y | rhs } - } -} - -impl BitAnd for Axes { - type Output = Self; - - fn bitand(self, rhs: Self) -> Self::Output { - Self { x: self.x & rhs.x, y: self.y & rhs.y } - } -} - -impl BitAnd for Axes { - type Output = Self; - - fn bitand(self, rhs: bool) -> Self::Output { - Self { x: self.x & rhs, y: self.y & rhs } - } -} - -impl BitOrAssign for Axes { - fn bitor_assign(&mut self, rhs: Self) { - self.x |= rhs.x; - self.y |= rhs.y; - } -} - -impl BitAndAssign for Axes { - fn bitand_assign(&mut self, rhs: Self) { - self.x &= rhs.x; - self.y &= rhs.y; - } -} - -cast! { - Axes>, - self => array![self.x, self.y].into_value(), - array: Array => { - let mut iter = array.into_iter(); - match (iter.next(), iter.next(), iter.next()) { - (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?), - _ => bail!("point array must contain exactly two entries"), - } - }, -} - -impl Resolve for Axes { - type Output = Axes; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.map(|v| v.resolve(styles)) - } -} - -impl Fold for Axes> { - type Output = Axes; - - fn fold(self, outer: Self::Output) -> Self::Output { - self.zip(outer).map(|(inner, outer)| match inner { - Some(value) => value.fold(outer), - None => outer, - }) - } -} diff --git a/src/geom/color.rs b/src/geom/color.rs deleted file mode 100644 index c7676c2b..00000000 --- a/src/geom/color.rs +++ /dev/null @@ -1,386 +0,0 @@ -use std::str::FromStr; - -use super::*; - -/// A color in a dynamic format. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum Color { - /// An 8-bit luma color. - Luma(LumaColor), - /// An 8-bit RGBA color. - Rgba(RgbaColor), - /// An 8-bit CMYK color. - Cmyk(CmykColor), -} - -impl Color { - pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); - pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); - pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); - pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); - pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); - pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); - pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); - pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); - pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); - pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); - pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); - pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); - pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); - pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); - pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); - pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); - pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); - pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); - - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { - match self { - Self::Luma(luma) => luma.to_rgba(), - Self::Rgba(rgba) => rgba, - Self::Cmyk(cmyk) => cmyk.to_rgba(), - } - } - - /// Lighten this color by the given factor. - pub fn lighten(self, factor: Ratio) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.lighten(factor)), - Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), - } - } - - /// Darken this color by the given factor. - pub fn darken(self, factor: Ratio) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.darken(factor)), - Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), - } - } - - /// Negate this color. - pub fn negate(self) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.negate()), - Self::Rgba(rgba) => Self::Rgba(rgba.negate()), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), - } - } -} - -impl Debug for Color { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Luma(c) => Debug::fmt(c, f), - Self::Rgba(c) => Debug::fmt(c, f), - Self::Cmyk(c) => Debug::fmt(c, f), - } - } -} - -/// An 8-bit grayscale color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct LumaColor(pub u8); - -impl LumaColor { - /// Construct a new luma color. - pub const fn new(luma: u8) -> Self { - Self(luma) - } - - /// Convert to an opque RGBA color. - pub const fn to_rgba(self) -> RgbaColor { - RgbaColor::new(self.0, self.0, self.0, u8::MAX) - } - - /// Convert to CMYK as a fraction of true black. - pub fn to_cmyk(self) -> CmykColor { - CmykColor::new( - round_u8(self.0 as f64 * 0.75), - round_u8(self.0 as f64 * 0.68), - round_u8(self.0 as f64 * 0.67), - round_u8(self.0 as f64 * 0.90), - ) - } - - /// Lighten this color by a factor. - pub fn lighten(self, factor: Ratio) -> Self { - let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); - Self(self.0.saturating_add(inc)) - } - - /// Darken this color by a factor. - pub fn darken(self, factor: Ratio) -> Self { - let dec = round_u8(self.0 as f64 * factor.get()); - Self(self.0.saturating_sub(dec)) - } - - /// Negate this color. - pub fn negate(self) -> Self { - Self(u8::MAX - self.0) - } -} - -impl Debug for LumaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "luma({})", self.0) - } -} - -impl From for Color { - fn from(luma: LumaColor) -> Self { - Self::Luma(luma) - } -} - -/// An 8-bit RGBA color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct RgbaColor { - /// Red channel. - pub r: u8, - /// Green channel. - pub g: u8, - /// Blue channel. - pub b: u8, - /// Alpha channel. - pub a: u8, -} - -impl RgbaColor { - /// Construct a new RGBA color. - pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { - Self { r, g, b, a } - } - - /// Lighten this color by a factor. - /// - /// The alpha channel is not affected. - pub fn lighten(self, factor: Ratio) -> Self { - let lighten = - |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); - Self { - r: lighten(self.r), - g: lighten(self.g), - b: lighten(self.b), - a: self.a, - } - } - - /// Darken this color by a factor. - /// - /// The alpha channel is not affected. - pub fn darken(self, factor: Ratio) -> Self { - let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); - Self { - r: darken(self.r), - g: darken(self.g), - b: darken(self.b), - a: self.a, - } - } - - /// Negate this color. - /// - /// The alpha channel is not affected. - pub fn negate(self) -> Self { - Self { - r: u8::MAX - self.r, - g: u8::MAX - self.g, - b: u8::MAX - self.b, - a: self.a, - } - } -} - -impl FromStr for RgbaColor { - type Err = &'static str; - - /// Constructs a new color from hex strings like the following: - /// - `#aef` (shorthand, with leading hashtag), - /// - `7a03c2` (without alpha), - /// - `abcdefff` (with alpha). - /// - /// The hashtag is optional and both lower and upper case are fine. - fn from_str(hex_str: &str) -> Result { - let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); - if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { - return Err("color string contains non-hexadecimal letters"); - } - - let len = hex_str.len(); - let long = len == 6 || len == 8; - let short = len == 3 || len == 4; - let alpha = len == 4 || len == 8; - if !long && !short { - return Err("color string has wrong length"); - } - - let mut values: [u8; 4] = [u8::MAX; 4]; - for elem in if alpha { 0..4 } else { 0..3 } { - let item_len = if long { 2 } else { 1 }; - let pos = elem * item_len; - - let item = &hex_str[pos..(pos + item_len)]; - values[elem] = u8::from_str_radix(item, 16).unwrap(); - - if short { - // Duplicate number for shorthand notation, i.e. `a` -> `aa` - values[elem] += values[elem] * 16; - } - } - - Ok(Self::new(values[0], values[1], values[2], values[3])) - } -} - -impl Debug for RgbaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if f.alternate() { - write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; - } else { - write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; - if self.a != 255 { - write!(f, "{:02x}", self.a)?; - } - write!(f, "\")")?; - } - Ok(()) - } -} - -impl> From for Color { - fn from(rgba: T) -> Self { - Self::Rgba(rgba.into()) - } -} - -cast! { - RgbaColor, - self => Value::Color(self.into()), -} - -/// An 8-bit CMYK color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct CmykColor { - /// The cyan component. - pub c: u8, - /// The magenta component. - pub m: u8, - /// The yellow component. - pub y: u8, - /// The key (black) component. - pub k: u8, -} - -impl CmykColor { - /// Construct a new CMYK color. - pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { - Self { c, m, y, k } - } - - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { - let k = self.k as f64 / 255.0; - let f = |c| { - let c = c as f64 / 255.0; - round_u8(255.0 * (1.0 - c) * (1.0 - k)) - }; - - RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } - } - - /// Lighten this color by a factor. - pub fn lighten(self, factor: Ratio) -> Self { - let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); - Self { - c: lighten(self.c), - m: lighten(self.m), - y: lighten(self.y), - k: lighten(self.k), - } - } - - /// Darken this color by a factor. - pub fn darken(self, factor: Ratio) -> Self { - let darken = - |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); - Self { - c: darken(self.c), - m: darken(self.m), - y: darken(self.y), - k: darken(self.k), - } - } - - /// Negate this color. - /// - /// Does not affect the key component. - pub fn negate(self) -> Self { - Self { - c: u8::MAX - self.c, - m: u8::MAX - self.m, - y: u8::MAX - self.y, - k: self.k, - } - } -} - -impl Debug for CmykColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let g = |c| 100.0 * (c as f64 / 255.0); - write!( - f, - "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", - g(self.c), - g(self.m), - g(self.y), - g(self.k), - ) - } -} - -impl From for Color { - fn from(cmyk: CmykColor) -> Self { - Self::Cmyk(cmyk) - } -} - -/// Convert to the closest u8. -fn round_u8(value: f64) -> u8 { - value.round() as u8 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_color_strings() { - #[track_caller] - fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { - assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); - } - - test("f61243ff", 0xf6, 0x12, 0x43, 0xff); - test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); - test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); - test("233", 0x22, 0x33, 0x33, 0xff); - test("111b", 0x11, 0x11, 0x11, 0xbb); - } - - #[test] - fn test_parse_invalid_colors() { - #[track_caller] - fn test(hex: &str, message: &str) { - assert_eq!(RgbaColor::from_str(hex), Err(message)); - } - - test("a5", "color string has wrong length"); - test("12345", "color string has wrong length"); - test("f075ff011", "color string has wrong length"); - test("hmmm", "color string contains non-hexadecimal letters"); - test("14B2AH", "color string contains non-hexadecimal letters"); - } -} diff --git a/src/geom/corners.rs b/src/geom/corners.rs deleted file mode 100644 index 5ee1e063..00000000 --- a/src/geom/corners.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; - -use super::*; - -/// A container with components for the four corners of a rectangle. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Corners { - /// The value for the top left corner. - pub top_left: T, - /// The value for the top right corner. - pub top_right: T, - /// The value for the bottom right corner. - pub bottom_right: T, - /// The value for the bottom left corner. - pub bottom_left: T, -} - -impl Corners { - /// Create a new instance from the four components. - pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self { - Self { top_left, top_right, bottom_right, bottom_left } - } - - /// Create an instance with four equal components. - pub fn splat(value: T) -> Self - where - T: Clone, - { - Self { - top_left: value.clone(), - top_right: value.clone(), - bottom_right: value.clone(), - bottom_left: value, - } - } - - /// Map the individual fields with `f`. - pub fn map(self, mut f: F) -> Corners - where - F: FnMut(T) -> U, - { - Corners { - top_left: f(self.top_left), - top_right: f(self.top_right), - bottom_right: f(self.bottom_right), - bottom_left: f(self.bottom_left), - } - } - - /// Zip two instances into one. - pub fn zip(self, other: Corners) -> Corners<(T, U)> { - Corners { - top_left: (self.top_left, other.top_left), - top_right: (self.top_right, other.top_right), - bottom_right: (self.bottom_right, other.bottom_right), - bottom_left: (self.bottom_left, other.bottom_left), - } - } - - /// An iterator over the corners, starting with the top left corner, - /// clockwise. - pub fn iter(&self) -> impl Iterator { - [&self.top_left, &self.top_right, &self.bottom_right, &self.bottom_left] - .into_iter() - } - - /// Whether all sides are equal. - pub fn is_uniform(&self) -> bool - where - T: PartialEq, - { - self.top_left == self.top_right - && self.top_right == self.bottom_right - && self.bottom_right == self.bottom_left - } -} - -impl Get for Corners { - type Component = T; - - fn get_ref(&self, corner: Corner) -> &T { - match corner { - Corner::TopLeft => &self.top_left, - Corner::TopRight => &self.top_right, - Corner::BottomRight => &self.bottom_right, - Corner::BottomLeft => &self.bottom_left, - } - } - - fn get_mut(&mut self, corner: Corner) -> &mut T { - match corner { - Corner::TopLeft => &mut self.top_left, - Corner::TopRight => &mut self.top_right, - Corner::BottomRight => &mut self.bottom_right, - Corner::BottomLeft => &mut self.bottom_left, - } - } -} - -/// The four corners of a rectangle. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Corner { - /// The top left corner. - TopLeft, - /// The top right corner. - TopRight, - /// The bottom right corner. - BottomRight, - /// The bottom left corner. - BottomLeft, -} - -impl Reflect for Corners> { - fn describe() -> CastInfo { - T::describe() + Dict::describe() - } - - fn castable(value: &Value) -> bool { - Dict::castable(value) || T::castable(value) - } -} - -impl IntoValue for Corners -where - T: PartialEq + IntoValue, -{ - fn into_value(self) -> Value { - if self.is_uniform() { - return self.top_left.into_value(); - } - - let mut dict = Dict::new(); - let mut handle = |key: &str, component: T| { - let value = component.into_value(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("top-left", self.top_left); - handle("top-right", self.top_right); - handle("bottom-right", self.bottom_right); - handle("bottom-left", self.bottom_left); - - Value::Dict(dict) - } -} - -impl FromValue for Corners> -where - T: FromValue + Clone, -{ - fn from_value(mut value: Value) -> StrResult { - let keys = [ - "top-left", - "top-right", - "bottom-right", - "bottom-left", - "left", - "top", - "right", - "bottom", - "rest", - ]; - - if let Value::Dict(dict) = &mut value { - if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { - let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); - let rest = take("rest")?; - let left = take("left")?.or_else(|| rest.clone()); - let top = take("top")?.or_else(|| rest.clone()); - let right = take("right")?.or_else(|| rest.clone()); - let bottom = take("bottom")?.or_else(|| rest.clone()); - let corners = Corners { - top_left: take("top-left")? - .or_else(|| top.clone()) - .or_else(|| left.clone()), - top_right: take("top-right")? - .or_else(|| top.clone()) - .or_else(|| right.clone()), - bottom_right: take("bottom-right")? - .or_else(|| bottom.clone()) - .or_else(|| right.clone()), - bottom_left: take("bottom-left")? - .or_else(|| bottom.clone()) - .or_else(|| left.clone()), - }; - - dict.finish(&keys)?; - return Ok(corners); - } - } - - if T::castable(&value) { - Ok(Self::splat(Some(T::from_value(value)?))) - } else { - Err(Self::error(&value)) - } - } -} - -impl Resolve for Corners { - type Output = Corners; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.map(|v| v.resolve(styles)) - } -} - -impl Fold for Corners> { - type Output = Corners; - - fn fold(self, outer: Self::Output) -> Self::Output { - self.zip(outer).map(|(inner, outer)| match inner { - Some(value) => value.fold(outer), - None => outer, - }) - } -} diff --git a/src/geom/dir.rs b/src/geom/dir.rs deleted file mode 100644 index 48915471..00000000 --- a/src/geom/dir.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::*; - -/// The four directions into which content can be laid out. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum Dir { - /// Left to right. - LTR, - /// Right to left. - RTL, - /// Top to bottom. - TTB, - /// Bottom to top. - BTT, -} - -impl Dir { - /// The specific axis this direction belongs to. - pub const fn axis(self) -> Axis { - match self { - Self::LTR | Self::RTL => Axis::X, - Self::TTB | Self::BTT => Axis::Y, - } - } - - /// The side this direction starts at. - pub const fn start(self) -> Side { - match self { - Self::LTR => Side::Left, - Self::RTL => Side::Right, - Self::TTB => Side::Top, - Self::BTT => Side::Bottom, - } - } - - /// The side this direction ends at. - pub const fn end(self) -> Side { - match self { - Self::LTR => Side::Right, - Self::RTL => Side::Left, - Self::TTB => Side::Bottom, - Self::BTT => Side::Top, - } - } - - /// The inverse direction. - pub const fn inv(self) -> Self { - match self { - Self::LTR => Self::RTL, - Self::RTL => Self::LTR, - Self::TTB => Self::BTT, - Self::BTT => Self::TTB, - } - } - - /// Whether this direction points into the positive coordinate direction. - /// - /// The positive directions are left-to-right and top-to-bottom. - pub const fn is_positive(self) -> bool { - match self { - Self::LTR | Self::TTB => true, - Self::RTL | Self::BTT => false, - } - } -} - -impl Debug for Dir { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::LTR => "ltr", - Self::RTL => "rtl", - Self::TTB => "ttb", - Self::BTT => "btt", - }) - } -} - -cast! { - type Dir: "direction", -} diff --git a/src/geom/ellipse.rs b/src/geom/ellipse.rs deleted file mode 100644 index ac20ffd3..00000000 --- a/src/geom/ellipse.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::*; - -/// Produce a shape that approximates an axis-aligned ellipse. -pub fn ellipse(size: Size, fill: Option, stroke: Option) -> Shape { - // https://stackoverflow.com/a/2007782 - let z = Abs::zero(); - let rx = size.x / 2.0; - let ry = size.y / 2.0; - let m = 0.551784; - let mx = m * rx; - let my = m * ry; - let point = |x, y| Point::new(x + rx, y + ry); - - let mut path = Path::new(); - path.move_to(point(-rx, z)); - path.cubic_to(point(-rx, -my), point(-mx, -ry), point(z, -ry)); - path.cubic_to(point(mx, -ry), point(rx, -my), point(rx, z)); - path.cubic_to(point(rx, my), point(mx, ry), point(z, ry)); - path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z)); - - Shape { geometry: Geometry::Path(path), stroke, fill } -} diff --git a/src/geom/em.rs b/src/geom/em.rs deleted file mode 100644 index 8dda9ff6..00000000 --- a/src/geom/em.rs +++ /dev/null @@ -1,153 +0,0 @@ -use super::*; - -/// A length that is relative to the font size. -/// -/// `1em` is the same as the font size. -#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Em(Scalar); - -impl Em { - /// The zero em length. - pub const fn zero() -> Self { - Self(Scalar(0.0)) - } - - /// The font size. - pub const fn one() -> Self { - Self(Scalar(1.0)) - } - - /// Create a font-relative length. - pub const fn new(em: f64) -> Self { - Self(Scalar(em)) - } - - /// Create an em length from font units at the given units per em. - pub fn from_units(units: impl Into, units_per_em: f64) -> Self { - Self(Scalar(units.into() / units_per_em)) - } - - /// Create an em length from a length at the given font size. - pub fn from_length(length: Abs, font_size: Abs) -> Self { - let result = length / font_size; - if result.is_finite() { - Self(Scalar(result)) - } else { - Self::zero() - } - } - - /// The number of em units. - pub const fn get(self) -> f64 { - (self.0).0 - } - - /// The absolute value of this em length. - pub fn abs(self) -> Self { - Self::new(self.get().abs()) - } - - /// Convert to an absolute length at the given font size. - pub fn at(self, font_size: Abs) -> Abs { - let resolved = font_size * self.get(); - if resolved.is_finite() { - resolved - } else { - Abs::zero() - } - } -} - -impl Numeric for Em { - fn zero() -> Self { - Self::zero() - } - - fn is_finite(self) -> bool { - self.0.is_finite() - } -} - -impl Debug for Em { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}em", self.get()) - } -} - -impl Neg for Em { - type Output = Self; - - fn neg(self) -> Self { - Self(-self.0) - } -} - -impl Add for Em { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self(self.0 + other.0) - } -} - -sub_impl!(Em - Em -> Em); - -impl Mul for Em { - type Output = Self; - - fn mul(self, other: f64) -> Self { - Self(self.0 * other) - } -} - -impl Mul for f64 { - type Output = Em; - - fn mul(self, other: Em) -> Em { - other * self - } -} - -impl Div for Em { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self(self.0 / other) - } -} - -impl Div for Em { - type Output = f64; - - fn div(self, other: Self) -> f64 { - self.get() / other.get() - } -} - -assign_impl!(Em += Em); -assign_impl!(Em -= Em); -assign_impl!(Em *= f64); -assign_impl!(Em /= f64); - -impl Sum for Em { - fn sum>(iter: I) -> Self { - Self(iter.map(|s| s.0).sum()) - } -} - -cast! { - Em, - self => Value::Length(self.into()), -} - -impl Resolve for Em { - type Output = Abs; - - fn resolve(self, styles: StyleChain) -> Self::Output { - if self.is_zero() { - Abs::zero() - } else { - self.at(item!(em)(styles)) - } - } -} diff --git a/src/geom/fr.rs b/src/geom/fr.rs deleted file mode 100644 index c602634d..00000000 --- a/src/geom/fr.rs +++ /dev/null @@ -1,119 +0,0 @@ -use super::*; - -/// A fraction of remaining space. -#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Fr(Scalar); - -impl Fr { - /// Takes up zero space: `0fr`. - pub const fn zero() -> Self { - Self(Scalar(0.0)) - } - - /// Takes up as much space as all other items with this fraction: `1fr`. - pub const fn one() -> Self { - Self(Scalar(1.0)) - } - - /// Create a new fraction. - pub const fn new(ratio: f64) -> Self { - Self(Scalar(ratio)) - } - - /// Get the underlying number. - pub const fn get(self) -> f64 { - (self.0).0 - } - - /// The absolute value of this fraction. - pub fn abs(self) -> Self { - Self::new(self.get().abs()) - } - - /// Determine this fraction's share in the remaining space. - pub fn share(self, total: Self, remaining: Abs) -> Abs { - let ratio = self / total; - if ratio.is_finite() && remaining.is_finite() { - (ratio * remaining).max(Abs::zero()) - } else { - Abs::zero() - } - } -} - -impl Numeric for Fr { - fn zero() -> Self { - Self::zero() - } - - fn is_finite(self) -> bool { - self.0.is_finite() - } -} - -impl Debug for Fr { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}fr", round_2(self.get())) - } -} - -impl Neg for Fr { - type Output = Self; - - fn neg(self) -> Self { - Self(-self.0) - } -} - -impl Add for Fr { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self(self.0 + other.0) - } -} - -sub_impl!(Fr - Fr -> Fr); - -impl Mul for Fr { - type Output = Self; - - fn mul(self, other: f64) -> Self { - Self(self.0 * other) - } -} - -impl Mul for f64 { - type Output = Fr; - - fn mul(self, other: Fr) -> Fr { - other * self - } -} - -impl Div for Fr { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self(self.0 / other) - } -} - -impl Div for Fr { - type Output = f64; - - fn div(self, other: Self) -> f64 { - self.get() / other.get() - } -} - -assign_impl!(Fr += Fr); -assign_impl!(Fr -= Fr); -assign_impl!(Fr *= f64); -assign_impl!(Fr /= f64); - -impl Sum for Fr { - fn sum>(iter: I) -> Self { - Self(iter.map(|s| s.0).sum()) - } -} diff --git a/src/geom/length.rs b/src/geom/length.rs deleted file mode 100644 index 7d0a9841..00000000 --- a/src/geom/length.rs +++ /dev/null @@ -1,128 +0,0 @@ -use super::*; - -/// A size or distance, possibly expressed with contextual units. -/// -/// Currently supports absolute and font-relative units, but support could quite -/// easily be extended to other units. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Length { - /// The absolute part. - pub abs: Abs, - /// The font-relative part. - pub em: Em, -} - -impl Length { - /// The zero length. - pub const fn zero() -> Self { - Self { abs: Abs::zero(), em: Em::zero() } - } - - /// Try to compute the absolute value of the length. - pub fn try_abs(self) -> Option { - (self.abs.is_zero() || self.em.is_zero()) - .then(|| Self { abs: self.abs.abs(), em: self.em.abs() }) - } - - /// Try to divide two lengths. - pub fn try_div(self, other: Self) -> Option { - if self.abs.is_zero() && other.abs.is_zero() { - Some(self.em / other.em) - } else if self.em.is_zero() && other.em.is_zero() { - Some(self.abs / other.abs) - } else { - None - } - } -} - -impl Debug for Length { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match (self.abs.is_zero(), self.em.is_zero()) { - (false, false) => write!(f, "{:?} + {:?}", self.abs, self.em), - (true, false) => self.em.fmt(f), - (_, true) => self.abs.fmt(f), - } - } -} - -impl Numeric for Length { - fn zero() -> Self { - Self::zero() - } - - fn is_finite(self) -> bool { - self.abs.is_finite() && self.em.is_finite() - } -} - -impl PartialOrd for Length { - fn partial_cmp(&self, other: &Self) -> Option { - if self.em.is_zero() && other.em.is_zero() { - self.abs.partial_cmp(&other.abs) - } else if self.abs.is_zero() && other.abs.is_zero() { - self.em.partial_cmp(&other.em) - } else { - None - } - } -} - -impl From for Length { - fn from(abs: Abs) -> Self { - Self { abs, em: Em::zero() } - } -} - -impl From for Length { - fn from(em: Em) -> Self { - Self { abs: Abs::zero(), em } - } -} - -impl Neg for Length { - type Output = Self; - - fn neg(self) -> Self::Output { - Self { abs: -self.abs, em: -self.em } - } -} - -impl Add for Length { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self { abs: self.abs + rhs.abs, em: self.em + rhs.em } - } -} - -sub_impl!(Length - Length -> Length); - -impl Mul for Length { - type Output = Self; - - fn mul(self, rhs: f64) -> Self::Output { - Self { abs: self.abs * rhs, em: self.em * rhs } - } -} - -impl Div for Length { - type Output = Self; - - fn div(self, rhs: f64) -> Self::Output { - Self { abs: self.abs / rhs, em: self.em / rhs } - } -} - -assign_impl!(Length += Length); -assign_impl!(Length -= Length); -assign_impl!(Length *= f64); -assign_impl!(Length /= f64); - -impl Resolve for Length { - type Output = Abs; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.abs + self.em.resolve(styles) - } -} diff --git a/src/geom/macros.rs b/src/geom/macros.rs deleted file mode 100644 index b1b50e22..00000000 --- a/src/geom/macros.rs +++ /dev/null @@ -1,47 +0,0 @@ -/// Implement the `Sub` trait based on existing `Neg` and `Add` impls. -macro_rules! sub_impl { - ($a:ident - $b:ident -> $c:ident) => { - impl std::ops::Sub<$b> for $a { - type Output = $c; - - fn sub(self, other: $b) -> $c { - self + -other - } - } - }; -} - -/// Implement an assign trait based on an existing non-assign trait. -macro_rules! assign_impl { - ($a:ident += $b:ident) => { - impl std::ops::AddAssign<$b> for $a { - fn add_assign(&mut self, other: $b) { - *self = *self + other; - } - } - }; - - ($a:ident -= $b:ident) => { - impl std::ops::SubAssign<$b> for $a { - fn sub_assign(&mut self, other: $b) { - *self = *self - other; - } - } - }; - - ($a:ident *= $b:ident) => { - impl std::ops::MulAssign<$b> for $a { - fn mul_assign(&mut self, other: $b) { - *self = *self * other; - } - } - }; - - ($a:ident /= $b:ident) => { - impl std::ops::DivAssign<$b> for $a { - fn div_assign(&mut self, other: $b) { - *self = *self / other; - } - } - }; -} diff --git a/src/geom/mod.rs b/src/geom/mod.rs deleted file mode 100644 index b7a7ff40..00000000 --- a/src/geom/mod.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Geometrical primitives. - -#[macro_use] -mod macros; -mod abs; -mod align; -mod angle; -mod axes; -mod color; -mod corners; -mod dir; -mod ellipse; -mod em; -mod fr; -mod length; -mod paint; -mod path; -mod point; -mod ratio; -mod rel; -mod rounded; -mod scalar; -mod shape; -mod sides; -mod size; -mod smart; -mod stroke; -mod transform; - -pub use self::abs::{Abs, AbsUnit}; -pub use self::align::{Align, GenAlign, HorizontalAlign, VerticalAlign}; -pub use self::angle::{Angle, AngleUnit}; -pub use self::axes::{Axes, Axis}; -pub use self::color::{CmykColor, Color, LumaColor, RgbaColor}; -pub use self::corners::{Corner, Corners}; -pub use self::dir::Dir; -pub use self::ellipse::ellipse; -pub use self::em::Em; -pub use self::fr::Fr; -pub use self::length::Length; -pub use self::paint::Paint; -pub use self::path::{Path, PathItem}; -pub use self::point::Point; -pub use self::ratio::Ratio; -pub use self::rel::Rel; -pub use self::rounded::rounded_rect; -pub use self::scalar::Scalar; -pub use self::shape::{Geometry, Shape}; -pub use self::sides::{Side, Sides}; -pub use self::size::Size; -pub use self::smart::Smart; -pub use self::stroke::{ - DashLength, DashPattern, LineCap, LineJoin, PartialStroke, Stroke, -}; -pub use self::transform::Transform; - -use std::cmp::Ordering; -use std::f64::consts::PI; -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::iter::Sum; -use std::ops::*; - -use crate::diag::{bail, StrResult}; -use crate::eval::{array, cast, Array, Dict, Value}; -use crate::model::{Fold, Resolve, StyleChain}; - -/// Generic access to a structure's components. -pub trait Get { - /// The structure's component type. - type Component; - - /// Borrow the component for the specified index. - fn get_ref(&self, index: Index) -> &Self::Component; - - /// Borrow the component for the specified index mutably. - fn get_mut(&mut self, index: Index) -> &mut Self::Component; - - /// Convenience method for getting a copy of a component. - fn get(self, index: Index) -> Self::Component - where - Self: Sized, - Self::Component: Copy, - { - *self.get_ref(index) - } - - /// Convenience method for setting a component. - fn set(&mut self, index: Index, component: Self::Component) { - *self.get_mut(index) = component; - } -} - -/// A numeric type. -pub trait Numeric: - Sized - + Debug - + Copy - + PartialEq - + Neg - + Add - + Sub - + Mul - + Div -{ - /// The identity element for addition. - fn zero() -> Self; - - /// Whether `self` is zero. - fn is_zero(self) -> bool { - self == Self::zero() - } - - /// Whether `self` consists only of finite parts. - fn is_finite(self) -> bool; -} - -/// Round a float to two decimal places. -pub fn round_2(value: f64) -> f64 { - (value * 100.0).round() / 100.0 -} diff --git a/src/geom/paint.rs b/src/geom/paint.rs deleted file mode 100644 index 10fa9fde..00000000 --- a/src/geom/paint.rs +++ /dev/null @@ -1,30 +0,0 @@ -use super::*; - -/// How a fill or stroke should be painted. -#[derive(Clone, Eq, PartialEq, Hash)] -pub enum Paint { - /// A solid color. - Solid(Color), -} - -impl> From for Paint { - fn from(t: T) -> Self { - Self::Solid(t.into()) - } -} - -impl Debug for Paint { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Solid(color) => color.fmt(f), - } - } -} - -cast! { - Paint, - self => match self { - Self::Solid(color) => Value::Color(color), - }, - color: Color => Self::Solid(color), -} diff --git a/src/geom/path.rs b/src/geom/path.rs deleted file mode 100644 index 1c5325a3..00000000 --- a/src/geom/path.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::*; - -/// A bezier path. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct Path(pub Vec); - -/// An item in a bezier path. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum PathItem { - MoveTo(Point), - LineTo(Point), - CubicTo(Point, Point, Point), - ClosePath, -} - -impl Path { - /// Create an empty path. - pub const fn new() -> Self { - Self(vec![]) - } - - /// Create a path that describes a rectangle. - pub fn rect(size: Size) -> Self { - let z = Abs::zero(); - let point = Point::new; - let mut path = Self::new(); - path.move_to(point(z, z)); - path.line_to(point(size.x, z)); - path.line_to(point(size.x, size.y)); - path.line_to(point(z, size.y)); - path.close_path(); - path - } - - /// Push a [`MoveTo`](PathItem::MoveTo) item. - pub fn move_to(&mut self, p: Point) { - self.0.push(PathItem::MoveTo(p)); - } - - /// Push a [`LineTo`](PathItem::LineTo) item. - pub fn line_to(&mut self, p: Point) { - self.0.push(PathItem::LineTo(p)); - } - - /// Push a [`CubicTo`](PathItem::CubicTo) item. - pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) { - self.0.push(PathItem::CubicTo(p1, p2, p3)); - } - - /// Push a [`ClosePath`](PathItem::ClosePath) item. - pub fn close_path(&mut self) { - self.0.push(PathItem::ClosePath); - } -} diff --git a/src/geom/point.rs b/src/geom/point.rs deleted file mode 100644 index e7811e1e..00000000 --- a/src/geom/point.rs +++ /dev/null @@ -1,146 +0,0 @@ -use super::*; - -/// A point in 2D. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Point { - /// The x coordinate. - pub x: Abs, - /// The y coordinate. - pub y: Abs, -} - -impl Point { - /// The origin point. - pub const fn zero() -> Self { - Self { x: Abs::zero(), y: Abs::zero() } - } - - /// Create a new point from x and y coordinates. - pub const fn new(x: Abs, y: Abs) -> Self { - Self { x, y } - } - - /// Create an instance with two equal components. - pub const fn splat(value: Abs) -> Self { - Self { x: value, y: value } - } - - /// Create a new point with y set to zero. - pub const fn with_x(x: Abs) -> Self { - Self { x, y: Abs::zero() } - } - - /// Create a new point with x set to zero. - pub const fn with_y(y: Abs) -> Self { - Self { x: Abs::zero(), y } - } - - /// The component-wise minimum of this and another point. - pub fn min(self, other: Self) -> Self { - Self { x: self.x.min(other.x), y: self.y.min(other.y) } - } - - /// The component-wise minimum of this and another point. - pub fn max(self, other: Self) -> Self { - Self { x: self.x.max(other.x), y: self.y.max(other.y) } - } - - /// The distance between this point and the origin. - pub fn hypot(self) -> Abs { - Abs::raw(self.x.to_raw().hypot(self.y.to_raw())) - } - - /// Transform the point with the given transformation. - pub fn transform(self, ts: Transform) -> Self { - Self::new( - ts.sx.of(self.x) + ts.kx.of(self.y) + ts.tx, - ts.ky.of(self.x) + ts.sy.of(self.y) + ts.ty, - ) - } - - /// Convert to a size. - pub fn to_size(self) -> Size { - Size::new(self.x, self.y) - } -} - -impl Numeric for Point { - fn zero() -> Self { - Self::zero() - } - - fn is_finite(self) -> bool { - self.x.is_finite() && self.y.is_finite() - } -} - -impl Get for Point { - type Component = Abs; - - fn get_ref(&self, axis: Axis) -> &Abs { - match axis { - Axis::X => &self.x, - Axis::Y => &self.y, - } - } - - fn get_mut(&mut self, axis: Axis) -> &mut Abs { - match axis { - Axis::X => &mut self.x, - Axis::Y => &mut self.y, - } - } -} - -impl Debug for Point { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Point({:?}, {:?})", self.x, self.y) - } -} - -impl Neg for Point { - type Output = Self; - - fn neg(self) -> Self { - Self { x: -self.x, y: -self.y } - } -} - -impl Add for Point { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self { x: self.x + other.x, y: self.y + other.y } - } -} - -sub_impl!(Point - Point -> Point); - -impl Mul for Point { - type Output = Self; - - fn mul(self, other: f64) -> Self { - Self { x: self.x * other, y: self.y * other } - } -} - -impl Mul for f64 { - type Output = Point; - - fn mul(self, other: Point) -> Point { - other * self - } -} - -impl Div for Point { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self { x: self.x / other, y: self.y / other } - } -} - -assign_impl!(Point += Point); -assign_impl!(Point -= Point); -assign_impl!(Point *= f64); -assign_impl!(Point /= f64); diff --git a/src/geom/ratio.rs b/src/geom/ratio.rs deleted file mode 100644 index fe87dd6c..00000000 --- a/src/geom/ratio.rs +++ /dev/null @@ -1,133 +0,0 @@ -use super::*; - -/// A ratio of a whole. -/// -/// _Note_: `50%` is represented as `0.5` here, but stored as `50.0` in the -/// corresponding [literal](crate::syntax::ast::Numeric). -#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Ratio(Scalar); - -impl Ratio { - /// A ratio of `0%` represented as `0.0`. - pub const fn zero() -> Self { - Self(Scalar(0.0)) - } - - /// A ratio of `100%` represented as `1.0`. - pub const fn one() -> Self { - Self(Scalar(1.0)) - } - - /// Create a new ratio from a value, where `1.0` means `100%`. - pub const fn new(ratio: f64) -> Self { - Self(Scalar(ratio)) - } - - /// Get the underlying ratio. - pub const fn get(self) -> f64 { - (self.0).0 - } - - /// Whether the ratio is zero. - pub fn is_zero(self) -> bool { - self.0 == 0.0 - } - - /// Whether the ratio is one. - pub fn is_one(self) -> bool { - self.0 == 1.0 - } - - /// The absolute value of this ratio. - pub fn abs(self) -> Self { - Self::new(self.get().abs()) - } - - /// Return the ratio of the given `whole`. - pub fn of(self, whole: T) -> T { - let resolved = whole * self.get(); - if resolved.is_finite() { - resolved - } else { - T::zero() - } - } -} - -impl Debug for Ratio { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}%", round_2(100.0 * self.get())) - } -} - -impl Neg for Ratio { - type Output = Self; - - fn neg(self) -> Self { - Self(-self.0) - } -} - -impl Add for Ratio { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self(self.0 + other.0) - } -} - -sub_impl!(Ratio - Ratio -> Ratio); - -impl Mul for Ratio { - type Output = Self; - - fn mul(self, other: Self) -> Self { - Self(self.0 * other.0) - } -} - -impl Mul for Ratio { - type Output = Self; - - fn mul(self, other: f64) -> Self { - Self(self.0 * other) - } -} - -impl Mul for f64 { - type Output = Ratio; - - fn mul(self, other: Ratio) -> Ratio { - other * self - } -} - -impl Div for Ratio { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self(self.0 / other) - } -} - -impl Div for f64 { - type Output = Self; - - fn div(self, other: Ratio) -> Self { - self / other.get() - } -} - -impl Div for Ratio { - type Output = f64; - - fn div(self, other: Self) -> f64 { - self.get() / other.get() - } -} - -assign_impl!(Ratio += Ratio); -assign_impl!(Ratio -= Ratio); -assign_impl!(Ratio *= Ratio); -assign_impl!(Ratio *= f64); -assign_impl!(Ratio /= f64); diff --git a/src/geom/rel.rs b/src/geom/rel.rs deleted file mode 100644 index 88972222..00000000 --- a/src/geom/rel.rs +++ /dev/null @@ -1,246 +0,0 @@ -use super::*; - -/// A value that is composed of a relative and an absolute part. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Rel { - /// The relative part. - pub rel: Ratio, - /// The absolute part. - pub abs: T, -} - -impl Rel { - /// The zero relative. - pub fn zero() -> Self { - Self { rel: Ratio::zero(), abs: T::zero() } - } - - /// A relative with a ratio of `100%` and no absolute part. - pub fn one() -> Self { - Self { rel: Ratio::one(), abs: T::zero() } - } - - /// Create a new relative from its parts. - pub fn new(rel: Ratio, abs: T) -> Self { - Self { rel, abs } - } - - /// Whether both parts are zero. - pub fn is_zero(self) -> bool { - self.rel.is_zero() && self.abs == T::zero() - } - - /// Whether the relative part is one and the absolute part is zero. - pub fn is_one(self) -> bool { - self.rel.is_one() && self.abs == T::zero() - } - - /// Evaluate this relative to the given `whole`. - pub fn relative_to(self, whole: T) -> T { - self.rel.of(whole) + self.abs - } - - /// Map the absolute part with `f`. - pub fn map(self, f: F) -> Rel - where - F: FnOnce(T) -> U, - U: Numeric, - { - Rel { rel: self.rel, abs: f(self.abs) } - } -} - -impl Rel { - /// Try to divide two relative lengths. - pub fn try_div(self, other: Self) -> Option { - if self.rel.is_zero() && other.rel.is_zero() { - self.abs.try_div(other.abs) - } else if self.abs.is_zero() && other.abs.is_zero() { - Some(self.rel / other.rel) - } else { - None - } - } -} - -impl Debug for Rel { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match (self.rel.is_zero(), self.abs.is_zero()) { - (false, false) => write!(f, "{:?} + {:?}", self.rel, self.abs), - (false, true) => self.rel.fmt(f), - (true, _) => self.abs.fmt(f), - } - } -} - -impl From for Rel { - fn from(abs: Abs) -> Self { - Rel::from(Length::from(abs)) - } -} - -impl From for Rel { - fn from(em: Em) -> Self { - Rel::from(Length::from(em)) - } -} - -impl From for Rel { - fn from(abs: T) -> Self { - Self { rel: Ratio::zero(), abs } - } -} - -impl From for Rel { - fn from(rel: Ratio) -> Self { - Self { rel, abs: T::zero() } - } -} - -impl PartialOrd for Rel { - fn partial_cmp(&self, other: &Self) -> Option { - if self.rel.is_zero() && other.rel.is_zero() { - self.abs.partial_cmp(&other.abs) - } else if self.abs.is_zero() && other.abs.is_zero() { - self.rel.partial_cmp(&other.rel) - } else { - None - } - } -} - -impl Neg for Rel { - type Output = Self; - - fn neg(self) -> Self { - Self { rel: -self.rel, abs: -self.abs } - } -} - -impl Add for Rel { - type Output = Self; - - fn add(self, other: Self) -> Self::Output { - Self { - rel: self.rel + other.rel, - abs: self.abs + other.abs, - } - } -} - -impl Sub for Rel { - type Output = Self; - - fn sub(self, other: Self) -> Self::Output { - self + -other - } -} - -impl Mul for Rel { - type Output = Self; - - fn mul(self, other: f64) -> Self::Output { - Self { rel: self.rel * other, abs: self.abs * other } - } -} - -impl Mul> for f64 { - type Output = Rel; - - fn mul(self, other: Rel) -> Self::Output { - other * self - } -} - -impl Div for Rel { - type Output = Self; - - fn div(self, other: f64) -> Self::Output { - Self { rel: self.rel / other, abs: self.abs / other } - } -} - -impl AddAssign for Rel { - fn add_assign(&mut self, other: Self) { - self.rel += other.rel; - self.abs += other.abs; - } -} - -impl SubAssign for Rel { - fn sub_assign(&mut self, other: Self) { - self.rel -= other.rel; - self.abs -= other.abs; - } -} - -impl> MulAssign for Rel { - fn mul_assign(&mut self, other: f64) { - self.rel *= other; - self.abs *= other; - } -} - -impl> DivAssign for Rel { - fn div_assign(&mut self, other: f64) { - self.rel /= other; - self.abs /= other; - } -} - -impl Add for Ratio { - type Output = Rel; - - fn add(self, other: T) -> Self::Output { - Rel::from(self) + Rel::from(other) - } -} - -impl Add for Rel { - type Output = Self; - - fn add(self, other: T) -> Self::Output { - self + Rel::from(other) - } -} - -impl Add for Rel { - type Output = Self; - - fn add(self, other: Ratio) -> Self::Output { - self + Rel::from(other) - } -} - -impl Resolve for Rel -where - T: Resolve + Numeric, - ::Output: Numeric, -{ - type Output = Rel<::Output>; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.map(|abs| abs.resolve(styles)) - } -} - -impl Fold for Rel { - type Output = Self; - - fn fold(self, _: Self::Output) -> Self::Output { - self - } -} - -impl Fold for Rel { - type Output = Self; - - fn fold(self, _: Self::Output) -> Self::Output { - self - } -} - -cast! { - Rel, - self => self.map(Length::from).into_value(), -} diff --git a/src/geom/rounded.rs b/src/geom/rounded.rs deleted file mode 100644 index f1a7ea08..00000000 --- a/src/geom/rounded.rs +++ /dev/null @@ -1,182 +0,0 @@ -use super::*; - -/// Produce shapes that together make up a rounded rectangle. -pub fn rounded_rect( - size: Size, - radius: Corners, - fill: Option, - stroke: Sides>, -) -> Vec { - let mut res = vec![]; - if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) { - res.push(Shape { - geometry: fill_geometry(size, radius), - fill, - stroke: if stroke.is_uniform() { stroke.top.clone() } else { None }, - }); - } - - if !stroke.is_uniform() { - for (path, stroke) in stroke_segments(size, radius, stroke) { - if stroke.is_some() { - res.push(Shape { geometry: Geometry::Path(path), fill: None, stroke }); - } - } - } - - res -} - -/// Output the shape of the rectangle as a path or primitive rectangle, -/// depending on whether it is rounded. -fn fill_geometry(size: Size, radius: Corners) -> Geometry { - if radius.iter().copied().all(Abs::is_zero) { - Geometry::Rect(size) - } else { - let mut paths = stroke_segments(size, radius, Sides::splat(None)); - assert_eq!(paths.len(), 1); - Geometry::Path(paths.pop().unwrap().0) - } -} - -/// Output the minimum number of paths along the rectangles border. -fn stroke_segments( - size: Size, - radius: Corners, - stroke: Sides>, -) -> Vec<(Path, Option)> { - let mut res = vec![]; - - let mut connection = Connection::default(); - let mut path = Path::new(); - let mut always_continuous = true; - let max_radius = size.x.min(size.y).max(Abs::zero()) / 2.0; - - for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] { - let continuous = stroke.get_ref(side) == stroke.get_ref(side.next_cw()); - connection = connection.advance(continuous && side != Side::Left); - always_continuous &= continuous; - - draw_side( - &mut path, - side, - size, - radius.get(side.start_corner()).clamp(Abs::zero(), max_radius), - radius.get(side.end_corner()).clamp(Abs::zero(), max_radius), - connection, - ); - - if !continuous { - res.push((std::mem::take(&mut path), stroke.get_ref(side).clone())); - } - } - - if always_continuous { - path.close_path(); - } - - if !path.0.is_empty() { - res.push((path, stroke.left)); - } - - res -} - -/// Draws one side of the rounded rectangle. Will always draw the left arc. The -/// right arc will be drawn halfway if and only if there is no connection. -fn draw_side( - path: &mut Path, - side: Side, - size: Size, - start_radius: Abs, - end_radius: Abs, - connection: Connection, -) { - let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 }); - let angle_right = Angle::deg(if connection.next { 90.0 } else { 45.0 }); - let length = size.get(side.axis()); - - // The arcs for a border of the rectangle along the x-axis, starting at (0,0). - let p1 = Point::with_x(start_radius); - let mut arc1 = bezier_arc( - p1 + Point::new( - -angle_left.sin() * start_radius, - (1.0 - angle_left.cos()) * start_radius, - ), - Point::new(start_radius, start_radius), - p1, - ); - - let p2 = Point::with_x(length - end_radius); - let mut arc2 = bezier_arc( - p2, - Point::new(length - end_radius, end_radius), - p2 + Point::new( - angle_right.sin() * end_radius, - (1.0 - angle_right.cos()) * end_radius, - ), - ); - - let transform = match side { - Side::Left => Transform::rotate(Angle::deg(-90.0)) - .post_concat(Transform::translate(Abs::zero(), size.y)), - Side::Bottom => Transform::rotate(Angle::deg(180.0)) - .post_concat(Transform::translate(size.x, size.y)), - Side::Right => Transform::rotate(Angle::deg(90.0)) - .post_concat(Transform::translate(size.x, Abs::zero())), - _ => Transform::identity(), - }; - - arc1 = arc1.map(|x| x.transform(transform)); - arc2 = arc2.map(|x| x.transform(transform)); - - if !connection.prev { - path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] }); - } - - if !start_radius.is_zero() { - path.cubic_to(arc1[1], arc1[2], arc1[3]); - } - - path.line_to(arc2[0]); - - if !connection.next && !end_radius.is_zero() { - path.cubic_to(arc2[1], arc2[2], arc2[3]); - } -} - -/// Get the control points for a bezier curve that describes a circular arc for -/// a start point, an end point and a center of the circle whose arc connects -/// the two. -fn bezier_arc(start: Point, center: Point, end: Point) -> [Point; 4] { - // https://stackoverflow.com/a/44829356/1567835 - let a = start - center; - let b = end - center; - - let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); - let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); - let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) - / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); - - let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); - let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); - - [start, control_1, control_2, end] -} - -/// Indicates which sides of the border strokes in a 2D polygon are connected to -/// their neighboring sides. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -struct Connection { - prev: bool, - next: bool, -} - -impl Connection { - /// Advance to the next clockwise side of the polygon. The argument - /// indicates whether the border is connected on the right side of the next - /// edge. - pub fn advance(self, next: bool) -> Self { - Self { prev: self.next, next } - } -} diff --git a/src/geom/scalar.rs b/src/geom/scalar.rs deleted file mode 100644 index 71fb1755..00000000 --- a/src/geom/scalar.rs +++ /dev/null @@ -1,175 +0,0 @@ -use super::*; - -/// A 64-bit float that implements `Eq`, `Ord` and `Hash`. -/// -/// Panics if it's `NaN` during any of those operations. -#[derive(Default, Copy, Clone)] -pub struct Scalar(pub f64); - -impl Numeric for Scalar { - fn zero() -> Self { - Self(0.0) - } - - fn is_finite(self) -> bool { - self.0.is_finite() - } -} - -impl From for Scalar { - fn from(float: f64) -> Self { - Self(float) - } -} - -impl From for f64 { - fn from(scalar: Scalar) -> Self { - scalar.0 - } -} - -impl Debug for Scalar { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl Eq for Scalar {} - -impl PartialEq for Scalar { - fn eq(&self, other: &Self) -> bool { - assert!(!self.0.is_nan() && !other.0.is_nan(), "float is NaN"); - self.0 == other.0 - } -} - -impl PartialEq for Scalar { - fn eq(&self, other: &f64) -> bool { - self == &Self(*other) - } -} - -impl Ord for Scalar { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).expect("float is NaN") - } -} - -impl PartialOrd for Scalar { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } - - fn lt(&self, other: &Self) -> bool { - self.0 < other.0 - } - - fn le(&self, other: &Self) -> bool { - self.0 <= other.0 - } - - fn gt(&self, other: &Self) -> bool { - self.0 > other.0 - } - - fn ge(&self, other: &Self) -> bool { - self.0 >= other.0 - } -} - -impl Hash for Scalar { - fn hash(&self, state: &mut H) { - debug_assert!(!self.0.is_nan(), "float is NaN"); - self.0.to_bits().hash(state); - } -} - -impl Neg for Scalar { - type Output = Self; - - fn neg(self) -> Self::Output { - Self(-self.0) - } -} - -impl> Add for Scalar { - type Output = Self; - - fn add(self, rhs: T) -> Self::Output { - Self(self.0 + rhs.into().0) - } -} - -impl> AddAssign for Scalar { - fn add_assign(&mut self, rhs: T) { - self.0 += rhs.into().0; - } -} - -impl> Sub for Scalar { - type Output = Self; - - fn sub(self, rhs: T) -> Self::Output { - Self(self.0 - rhs.into().0) - } -} - -impl> SubAssign for Scalar { - fn sub_assign(&mut self, rhs: T) { - self.0 -= rhs.into().0; - } -} - -impl> Mul for Scalar { - type Output = Self; - - fn mul(self, rhs: T) -> Self::Output { - Self(self.0 * rhs.into().0) - } -} - -impl> MulAssign for Scalar { - fn mul_assign(&mut self, rhs: T) { - self.0 *= rhs.into().0; - } -} - -impl> Div for Scalar { - type Output = Self; - - fn div(self, rhs: T) -> Self::Output { - Self(self.0 / rhs.into().0) - } -} - -impl> DivAssign for Scalar { - fn div_assign(&mut self, rhs: T) { - self.0 /= rhs.into().0; - } -} - -impl> Rem for Scalar { - type Output = Self; - - fn rem(self, rhs: T) -> Self::Output { - Self(self.0 % rhs.into().0) - } -} - -impl> RemAssign for Scalar { - fn rem_assign(&mut self, rhs: T) { - self.0 %= rhs.into().0; - } -} - -impl Sum for Scalar { - fn sum>(iter: I) -> Self { - Self(iter.map(|s| s.0).sum()) - } -} - -impl<'a> Sum<&'a Self> for Scalar { - fn sum>(iter: I) -> Self { - Self(iter.map(|s| s.0).sum()) - } -} diff --git a/src/geom/shape.rs b/src/geom/shape.rs deleted file mode 100644 index 5658c21f..00000000 --- a/src/geom/shape.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::*; - -/// A geometric shape with optional fill and stroke. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Shape { - /// The shape's geometry. - pub geometry: Geometry, - /// The shape's background fill. - pub fill: Option, - /// The shape's border stroke. - pub stroke: Option, -} - -/// A shape's geometry. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum Geometry { - /// A line to a point (relative to its position). - Line(Point), - /// A rectangle with its origin in the topleft corner. - Rect(Size), - /// A bezier path. - Path(Path), -} - -impl Geometry { - /// Fill the geometry without a stroke. - pub fn filled(self, fill: Paint) -> Shape { - Shape { geometry: self, fill: Some(fill), stroke: None } - } - - /// Stroke the geometry without a fill. - pub fn stroked(self, stroke: Stroke) -> Shape { - Shape { geometry: self, fill: None, stroke: Some(stroke) } - } -} diff --git a/src/geom/sides.rs b/src/geom/sides.rs deleted file mode 100644 index d4b72a9d..00000000 --- a/src/geom/sides.rs +++ /dev/null @@ -1,268 +0,0 @@ -use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; - -use super::*; - -/// A container with left, top, right and bottom components. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Sides { - /// The value for the left side. - pub left: T, - /// The value for the top side. - pub top: T, - /// The value for the right side. - pub right: T, - /// The value for the bottom side. - pub bottom: T, -} - -impl Sides { - /// Create a new instance from the four components. - pub const fn new(left: T, top: T, right: T, bottom: T) -> Self { - Self { left, top, right, bottom } - } - - /// Create an instance with four equal components. - pub fn splat(value: T) -> Self - where - T: Clone, - { - Self { - left: value.clone(), - top: value.clone(), - right: value.clone(), - bottom: value, - } - } - - /// Map the individual fields with `f`. - pub fn map(self, mut f: F) -> Sides - where - F: FnMut(T) -> U, - { - Sides { - left: f(self.left), - top: f(self.top), - right: f(self.right), - bottom: f(self.bottom), - } - } - - /// Zip two instances into one. - pub fn zip(self, other: Sides) -> Sides<(T, U)> { - Sides { - left: (self.left, other.left), - top: (self.top, other.top), - right: (self.right, other.right), - bottom: (self.bottom, other.bottom), - } - } - - /// An iterator over the sides, starting with the left side, clockwise. - pub fn iter(&self) -> impl Iterator { - [&self.left, &self.top, &self.right, &self.bottom].into_iter() - } - - /// Whether all sides are equal. - pub fn is_uniform(&self) -> bool - where - T: PartialEq, - { - self.left == self.top && self.top == self.right && self.right == self.bottom - } -} - -impl Sides { - /// Sums up `left` and `right` into `x`, and `top` and `bottom` into `y`. - pub fn sum_by_axis(self) -> Axes { - Axes::new(self.left + self.right, self.top + self.bottom) - } -} - -impl Sides> { - /// Evaluate the sides relative to the given `size`. - pub fn relative_to(self, size: Size) -> Sides { - Sides { - left: self.left.relative_to(size.x), - top: self.top.relative_to(size.y), - right: self.right.relative_to(size.x), - bottom: self.bottom.relative_to(size.y), - } - } -} - -impl Get for Sides { - type Component = T; - - fn get_ref(&self, side: Side) -> &T { - match side { - Side::Left => &self.left, - Side::Top => &self.top, - Side::Right => &self.right, - Side::Bottom => &self.bottom, - } - } - - fn get_mut(&mut self, side: Side) -> &mut T { - match side { - Side::Left => &mut self.left, - Side::Top => &mut self.top, - Side::Right => &mut self.right, - Side::Bottom => &mut self.bottom, - } - } -} - -/// The four sides of objects. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Side { - /// The left side. - Left, - /// The top side. - Top, - /// The right side. - Right, - /// The bottom side. - Bottom, -} - -impl Side { - /// The opposite side. - pub fn inv(self) -> Self { - match self { - Self::Left => Self::Right, - Self::Top => Self::Bottom, - Self::Right => Self::Left, - Self::Bottom => Self::Top, - } - } - - /// The next side, clockwise. - pub fn next_cw(self) -> Self { - match self { - Self::Left => Self::Top, - Self::Top => Self::Right, - Self::Right => Self::Bottom, - Self::Bottom => Self::Left, - } - } - - /// The next side, counter-clockwise. - pub fn next_ccw(self) -> Self { - match self { - Self::Left => Self::Bottom, - Self::Top => Self::Left, - Self::Right => Self::Top, - Self::Bottom => Self::Right, - } - } - - /// The first corner of the side in clockwise order. - pub fn start_corner(self) -> Corner { - match self { - Self::Left => Corner::BottomLeft, - Self::Top => Corner::TopLeft, - Self::Right => Corner::TopRight, - Self::Bottom => Corner::BottomRight, - } - } - - /// The second corner of the side in clockwise order. - pub fn end_corner(self) -> Corner { - self.next_cw().start_corner() - } - - /// Return the corresponding axis. - pub fn axis(self) -> Axis { - match self { - Self::Left | Self::Right => Axis::Y, - Self::Top | Self::Bottom => Axis::X, - } - } -} - -impl Reflect for Sides> { - fn describe() -> CastInfo { - T::describe() + Dict::describe() - } - - fn castable(value: &Value) -> bool { - Dict::castable(value) || T::castable(value) - } -} - -impl IntoValue for Sides -where - T: PartialEq + IntoValue, -{ - fn into_value(self) -> Value { - if self.is_uniform() { - return self.left.into_value(); - } - - let mut dict = Dict::new(); - let mut handle = |key: &str, component: T| { - let value = component.into_value(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("left", self.left); - handle("top", self.top); - handle("right", self.right); - handle("bottom", self.bottom); - - Value::Dict(dict) - } -} - -impl FromValue for Sides> -where - T: Default + FromValue + Clone, -{ - fn from_value(mut value: Value) -> StrResult { - let keys = ["left", "top", "right", "bottom", "x", "y", "rest"]; - if let Value::Dict(dict) = &mut value { - if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { - let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); - let rest = take("rest")?; - let x = take("x")?.or_else(|| rest.clone()); - let y = take("y")?.or_else(|| rest.clone()); - let sides = Sides { - left: take("left")?.or_else(|| x.clone()), - top: take("top")?.or_else(|| y.clone()), - right: take("right")?.or_else(|| x.clone()), - bottom: take("bottom")?.or_else(|| y.clone()), - }; - - dict.finish(&keys)?; - return Ok(sides); - } - } - - if T::castable(&value) { - Ok(Self::splat(Some(T::from_value(value)?))) - } else { - Err(Self::error(&value)) - } - } -} - -impl Resolve for Sides { - type Output = Sides; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.map(|v| v.resolve(styles)) - } -} - -impl Fold for Sides> { - type Output = Sides; - - fn fold(self, outer: Self::Output) -> Self::Output { - self.zip(outer).map(|(inner, outer)| match inner { - Some(value) => value.fold(outer), - None => outer, - }) - } -} diff --git a/src/geom/size.rs b/src/geom/size.rs deleted file mode 100644 index a2e32b77..00000000 --- a/src/geom/size.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::*; - -/// A size in 2D. -pub type Size = Axes; - -impl Size { - /// The zero value. - pub const fn zero() -> Self { - Self { x: Abs::zero(), y: Abs::zero() } - } - - /// Whether the other size fits into this one (smaller width and height). - pub fn fits(self, other: Self) -> bool { - self.x.fits(other.x) && self.y.fits(other.y) - } - - /// Convert to a point. - pub fn to_point(self) -> Point { - Point::new(self.x, self.y) - } -} - -impl Numeric for Size { - fn zero() -> Self { - Self::zero() - } - - fn is_finite(self) -> bool { - self.x.is_finite() && self.y.is_finite() - } -} - -impl Neg for Size { - type Output = Self; - - fn neg(self) -> Self { - Self { x: -self.x, y: -self.y } - } -} - -impl Add for Size { - type Output = Self; - - fn add(self, other: Self) -> Self { - Self { x: self.x + other.x, y: self.y + other.y } - } -} - -sub_impl!(Size - Size -> Size); - -impl Mul for Size { - type Output = Self; - - fn mul(self, other: f64) -> Self { - Self { x: self.x * other, y: self.y * other } - } -} - -impl Mul for f64 { - type Output = Size; - - fn mul(self, other: Size) -> Size { - other * self - } -} - -impl Div for Size { - type Output = Self; - - fn div(self, other: f64) -> Self { - Self { x: self.x / other, y: self.y / other } - } -} - -assign_impl!(Size -= Size); -assign_impl!(Size += Size); -assign_impl!(Size *= f64); -assign_impl!(Size /= f64); diff --git a/src/geom/smart.rs b/src/geom/smart.rs deleted file mode 100644 index a6271c20..00000000 --- a/src/geom/smart.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::eval::{AutoValue, CastInfo, FromValue, IntoValue, Reflect}; - -use super::*; - -/// A value that can be automatically determined. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum Smart { - /// The value should be determined smartly based on the circumstances. - Auto, - /// A specific value. - Custom(T), -} - -impl Smart { - /// Whether the value is `Auto`. - pub fn is_auto(&self) -> bool { - matches!(self, Self::Auto) - } - - /// Whether this holds a custom value. - pub fn is_custom(&self) -> bool { - matches!(self, Self::Custom(_)) - } - - /// Returns a reference the contained custom value. - /// If the value is [`Smart::Auto`], `None` is returned. - pub fn as_custom(self) -> Option { - match self { - Self::Auto => None, - Self::Custom(x) => Some(x), - } - } - - /// Map the contained custom value with `f`. - pub fn map(self, f: F) -> Smart - where - F: FnOnce(T) -> U, - { - match self { - Self::Auto => Smart::Auto, - Self::Custom(x) => Smart::Custom(f(x)), - } - } - - /// Map the contained custom value with `f` if it contains a custom value, - /// otherwise returns `default`. - pub fn map_or(self, default: U, f: F) -> U - where - F: FnOnce(T) -> U, - { - match self { - Self::Auto => default, - Self::Custom(x) => f(x), - } - } - - /// Keeps `self` if it contains a custom value, otherwise returns `other`. - pub fn or(self, other: Smart) -> Self { - match self { - Self::Custom(x) => Self::Custom(x), - Self::Auto => other, - } - } - - /// Returns the contained custom value or a provided default value. - pub fn unwrap_or(self, default: T) -> T { - match self { - Self::Auto => default, - Self::Custom(x) => x, - } - } - - /// Returns the contained custom value or computes a default value. - pub fn unwrap_or_else(self, f: F) -> T - where - F: FnOnce() -> T, - { - match self { - Self::Auto => f(), - Self::Custom(x) => x, - } - } - - /// Returns the contained custom value or the default value. - pub fn unwrap_or_default(self) -> T - where - T: Default, - { - self.unwrap_or_else(T::default) - } -} - -impl Default for Smart { - fn default() -> Self { - Self::Auto - } -} - -impl Reflect for Smart { - fn castable(value: &Value) -> bool { - AutoValue::castable(value) || T::castable(value) - } - - fn describe() -> CastInfo { - T::describe() + AutoValue::describe() - } -} - -impl IntoValue for Smart { - fn into_value(self) -> Value { - match self { - Smart::Custom(v) => v.into_value(), - Smart::Auto => Value::Auto, - } - } -} - -impl FromValue for Smart { - fn from_value(value: Value) -> StrResult { - match value { - Value::Auto => Ok(Self::Auto), - v if T::castable(&v) => Ok(Self::Custom(T::from_value(v)?)), - _ => Err(Self::error(&value)), - } - } -} - -impl Resolve for Smart { - type Output = Smart; - - fn resolve(self, styles: StyleChain) -> Self::Output { - self.map(|v| v.resolve(styles)) - } -} - -impl Fold for Smart -where - T: Fold, - T::Output: Default, -{ - type Output = Smart; - - fn fold(self, outer: Self::Output) -> Self::Output { - self.map(|inner| inner.fold(outer.unwrap_or_default())) - } -} diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs deleted file mode 100644 index 66264d5d..00000000 --- a/src/geom/stroke.rs +++ /dev/null @@ -1,387 +0,0 @@ -use crate::eval::{Cast, FromValue}; - -use super::*; - -/// A stroke of a geometric shape. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Stroke { - /// The stroke's paint. - pub paint: Paint, - /// The stroke's thickness. - pub thickness: Abs, - /// The stroke's line cap. - pub line_cap: LineCap, - /// The stroke's line join. - pub line_join: LineJoin, - /// The stroke's line dash pattern. - pub dash_pattern: Option>, - /// The miter limit. Defaults to 4.0, same as `tiny-skia`. - pub miter_limit: Scalar, -} - -impl Default for Stroke { - fn default() -> Self { - Self { - paint: Paint::Solid(Color::BLACK), - thickness: Abs::pt(1.0), - line_cap: LineCap::Butt, - line_join: LineJoin::Miter, - dash_pattern: None, - miter_limit: Scalar(4.0), - } - } -} - -/// A partial stroke representation. -/// -/// In this representation, both fields are optional so that you can pass either -/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where -/// this is expected. -#[derive(Default, Clone, Eq, PartialEq, Hash)] -pub struct PartialStroke { - /// The stroke's paint. - pub paint: Smart, - /// The stroke's thickness. - pub thickness: Smart, - /// The stroke's line cap. - pub line_cap: Smart, - /// The stroke's line join. - pub line_join: Smart, - /// The stroke's line dash pattern. - pub dash_pattern: Smart>>, - /// The miter limit. - pub miter_limit: Smart, -} - -impl PartialStroke { - /// Map the contained lengths with `f`. - pub fn map(self, f: F) -> PartialStroke - where - F: Fn(T) -> U, - { - PartialStroke { - paint: self.paint, - thickness: self.thickness.map(&f), - line_cap: self.line_cap, - line_join: self.line_join, - dash_pattern: self.dash_pattern.map(|pattern| { - pattern.map(|pattern| DashPattern { - array: pattern - .array - .into_iter() - .map(|l| match l { - DashLength::Length(v) => DashLength::Length(f(v)), - DashLength::LineWidth => DashLength::LineWidth, - }) - .collect(), - phase: f(pattern.phase), - }) - }), - miter_limit: self.miter_limit, - } - } -} - -impl PartialStroke { - /// Unpack the stroke, filling missing fields from the `default`. - pub fn unwrap_or(self, default: Stroke) -> Stroke { - let thickness = self.thickness.unwrap_or(default.thickness); - let dash_pattern = self - .dash_pattern - .map(|pattern| { - pattern.map(|pattern| DashPattern { - array: pattern - .array - .into_iter() - .map(|l| l.finish(thickness)) - .collect(), - phase: pattern.phase, - }) - }) - .unwrap_or(default.dash_pattern); - - Stroke { - paint: self.paint.unwrap_or(default.paint), - thickness, - line_cap: self.line_cap.unwrap_or(default.line_cap), - line_join: self.line_join.unwrap_or(default.line_join), - dash_pattern, - miter_limit: self.miter_limit.unwrap_or(default.miter_limit), - } - } - - /// Unpack the stroke, filling missing fields with the default values. - pub fn unwrap_or_default(self) -> Stroke { - self.unwrap_or(Stroke::default()) - } -} - -impl Debug for PartialStroke { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let Self { - paint, - thickness, - line_cap, - line_join, - dash_pattern, - miter_limit, - } = &self; - if line_cap.is_auto() - && line_join.is_auto() - && dash_pattern.is_auto() - && miter_limit.is_auto() - { - match (&self.paint, &self.thickness) { - (Smart::Custom(paint), Smart::Custom(thickness)) => { - write!(f, "{thickness:?} + {paint:?}") - } - (Smart::Custom(paint), Smart::Auto) => paint.fmt(f), - (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f), - (Smart::Auto, Smart::Auto) => f.pad("1pt + black"), - } - } else { - write!(f, "(")?; - let mut sep = ""; - if let Smart::Custom(paint) = &paint { - write!(f, "{}paint: {:?}", sep, paint)?; - sep = ", "; - } - if let Smart::Custom(thickness) = &thickness { - write!(f, "{}thickness: {:?}", sep, thickness)?; - sep = ", "; - } - if let Smart::Custom(cap) = &line_cap { - write!(f, "{}cap: {:?}", sep, cap)?; - sep = ", "; - } - if let Smart::Custom(join) = &line_join { - write!(f, "{}join: {:?}", sep, join)?; - sep = ", "; - } - if let Smart::Custom(dash) = &dash_pattern { - write!(f, "{}dash: {:?}", sep, dash)?; - sep = ", "; - } - if let Smart::Custom(miter_limit) = &miter_limit { - write!(f, "{}miter-limit: {:?}", sep, miter_limit)?; - } - write!(f, ")")?; - Ok(()) - } - } -} - -impl Resolve for PartialStroke { - type Output = PartialStroke; - - fn resolve(self, styles: StyleChain) -> Self::Output { - PartialStroke { - paint: self.paint, - thickness: self.thickness.resolve(styles), - line_cap: self.line_cap, - line_join: self.line_join, - dash_pattern: self.dash_pattern.resolve(styles), - miter_limit: self.miter_limit, - } - } -} - -impl Fold for PartialStroke { - type Output = Self; - - fn fold(self, outer: Self::Output) -> Self::Output { - Self { - paint: self.paint.or(outer.paint), - thickness: self.thickness.or(outer.thickness), - line_cap: self.line_cap.or(outer.line_cap), - line_join: self.line_join.or(outer.line_join), - dash_pattern: self.dash_pattern.or(outer.dash_pattern), - miter_limit: self.miter_limit.or(outer.miter_limit), - } - } -} - -cast! { - type PartialStroke: "stroke", - thickness: Length => Self { - thickness: Smart::Custom(thickness), - ..Default::default() - }, - color: Color => Self { - paint: Smart::Custom(color.into()), - ..Default::default() - }, - mut dict: Dict => { - fn take(dict: &mut Dict, key: &str) -> StrResult> { - Ok(dict.take(key).ok().map(T::from_value) - .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) - } - - let paint = take::(&mut dict, "paint")?; - let thickness = take::(&mut dict, "thickness")?; - let line_cap = take::(&mut dict, "cap")?; - let line_join = take::(&mut dict, "join")?; - let dash_pattern = take::>(&mut dict, "dash")?; - let miter_limit = take::(&mut dict, "miter-limit")?; - dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; - - Self { - paint, - thickness, - line_cap, - line_join, - dash_pattern, - miter_limit: miter_limit.map(Scalar), - } - }, -} - -cast! { - PartialStroke, - self => self.map(Length::from).into_value(), -} - -/// The line cap of a stroke -#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum LineCap { - Butt, - Round, - Square, -} - -impl Debug for LineCap { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - LineCap::Butt => write!(f, "\"butt\""), - LineCap::Round => write!(f, "\"round\""), - LineCap::Square => write!(f, "\"square\""), - } - } -} - -/// The line join of a stroke -#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum LineJoin { - Miter, - Round, - Bevel, -} - -impl Debug for LineJoin { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - LineJoin::Miter => write!(f, "\"miter\""), - LineJoin::Round => write!(f, "\"round\""), - LineJoin::Bevel => write!(f, "\"bevel\""), - } - } -} - -/// A line dash pattern. -#[derive(Clone, Eq, PartialEq, Hash)] -pub struct DashPattern> { - /// The dash array. - pub array: Vec
, - /// The dash phase. - pub phase: T, -} - -impl Debug for DashPattern { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "(array: (")?; - for (i, elem) in self.array.iter().enumerate() { - if i == 0 { - write!(f, "{:?}", elem)?; - } else { - write!(f, ", {:?}", elem)?; - } - } - write!(f, "), phase: {:?})", self.phase)?; - Ok(()) - } -} - -impl From>> for DashPattern { - fn from(array: Vec>) -> Self { - Self { array, phase: T::default() } - } -} - -impl Resolve for DashPattern { - type Output = DashPattern; - - fn resolve(self, styles: StyleChain) -> Self::Output { - DashPattern { - array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), - phase: self.phase.resolve(styles), - } - } -} - -// Same names as tikz: -// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns -cast! { - DashPattern, - - "solid" => Vec::new().into(), - "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(), - "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(), - "loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(), - "dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(), - "densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(), - "loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(), - "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), - "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), - "loosely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(), - - array: Vec => Self { array, phase: Length::zero() }, - mut dict: Dict => { - let array: Vec = dict.take("array")?.cast()?; - let phase = dict.take("phase").ok().map(Value::cast) - .transpose()?.unwrap_or(Length::zero()); - dict.finish(&["array", "phase"])?; - Self { - array, - phase, - } - }, -} - -/// The length of a dash in a line dash pattern -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum DashLength { - LineWidth, - Length(T), -} - -impl From for DashLength { - fn from(l: Abs) -> Self { - DashLength::Length(l.into()) - } -} - -impl DashLength { - fn finish(self, line_width: T) -> T { - match self { - Self::LineWidth => line_width, - Self::Length(l) => l, - } - } -} - -impl Resolve for DashLength { - type Output = DashLength; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Self::LineWidth => DashLength::LineWidth, - Self::Length(v) => DashLength::Length(v.resolve(styles)), - } - } -} - -cast! { - DashLength, - "dot" => Self::LineWidth, - v: Length => Self::Length(v), -} diff --git a/src/geom/transform.rs b/src/geom/transform.rs deleted file mode 100644 index 1ff1dfdd..00000000 --- a/src/geom/transform.rs +++ /dev/null @@ -1,77 +0,0 @@ -use super::*; - -/// A scale-skew-translate transformation. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Transform { - pub sx: Ratio, - pub ky: Ratio, - pub kx: Ratio, - pub sy: Ratio, - pub tx: Abs, - pub ty: Abs, -} - -impl Transform { - /// The identity transformation. - pub const fn identity() -> Self { - Self { - sx: Ratio::one(), - ky: Ratio::zero(), - kx: Ratio::zero(), - sy: Ratio::one(), - tx: Abs::zero(), - ty: Abs::zero(), - } - } - - /// A translate transform. - pub const fn translate(tx: Abs, ty: Abs) -> Self { - Self { tx, ty, ..Self::identity() } - } - - /// A scale transform. - pub const fn scale(sx: Ratio, sy: Ratio) -> Self { - Self { sx, sy, ..Self::identity() } - } - - /// A rotate transform. - pub fn rotate(angle: Angle) -> Self { - let cos = Ratio::new(angle.cos()); - let sin = Ratio::new(angle.sin()); - Self { - sx: cos, - ky: sin, - kx: -sin, - sy: cos, - ..Self::default() - } - } - - /// Whether this is the identity transformation. - pub fn is_identity(self) -> bool { - self == Self::identity() - } - - /// Pre-concatenate another transformation. - pub fn pre_concat(self, prev: Self) -> Self { - Transform { - sx: self.sx * prev.sx + self.kx * prev.ky, - ky: self.ky * prev.sx + self.sy * prev.ky, - kx: self.sx * prev.kx + self.kx * prev.sy, - sy: self.ky * prev.kx + self.sy * prev.sy, - tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx, - ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty, - } - } - - /// Post-concatenate another transformation. - pub fn post_concat(self, next: Self) -> Self { - next.pre_concat(self) - } -} - -impl Default for Transform { - fn default() -> Self { - Self::identity() - } -} diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs deleted file mode 100644 index dad466c1..00000000 --- a/src/ide/analyze.rs +++ /dev/null @@ -1,111 +0,0 @@ -use comemo::Track; -use ecow::EcoString; - -use crate::doc::Frame; -use crate::eval::{eval, Module, Route, Tracer, Value}; -use crate::model::{Introspector, Label}; -use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; -use crate::World; - -/// Try to determine a set of possible values for an expression. -pub fn analyze_expr(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { - match node.cast::() { - Some(ast::Expr::None(_)) => vec![Value::None], - Some(ast::Expr::Auto(_)) => vec![Value::Auto], - Some(ast::Expr::Bool(v)) => vec![Value::Bool(v.get())], - Some(ast::Expr::Int(v)) => vec![Value::Int(v.get())], - Some(ast::Expr::Float(v)) => vec![Value::Float(v.get())], - Some(ast::Expr::Numeric(v)) => vec![Value::numeric(v.get())], - Some(ast::Expr::Str(v)) => vec![Value::Str(v.get().into())], - - Some(ast::Expr::FieldAccess(access)) => { - let Some(child) = node.children().next() else { return vec![] }; - analyze_expr(world, &child) - .into_iter() - .filter_map(|target| target.field(&access.field()).ok()) - .collect() - } - - Some(_) => { - if let Some(parent) = node.parent() { - if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 { - return analyze_expr(world, parent); - } - } - - let route = Route::default(); - let mut tracer = Tracer::new(Some(node.span())); - typst::eval::eval( - world.track(), - route.track(), - tracer.track_mut(), - &world.main(), - ) - .and_then(|module| { - typst::model::typeset( - world.track(), - tracer.track_mut(), - &module.content(), - ) - }) - .ok(); - - tracer.finish() - } - - _ => vec![], - } -} - -/// Try to load a module from the current source file. -pub fn analyze_import( - world: &(dyn World + 'static), - source: &Source, - path: &str, -) -> Option { - let route = Route::default(); - let mut tracer = Tracer::default(); - let id = source.id().join(path).ok()?; - let source = world.source(id).ok()?; - eval(world.track(), route.track(), tracer.track_mut(), &source).ok() -} - -/// Find all labels and details for them. -/// -/// Returns: -/// - All labels and descriptions for them, if available -/// - A split offset: All labels before this offset belong to nodes, all after -/// belong to a bibliography. -pub fn analyze_labels( - world: &(dyn World + 'static), - frames: &[Frame], -) -> (Vec<(Label, Option)>, usize) { - let mut output = vec![]; - let introspector = Introspector::new(frames); - let items = &world.library().items; - - // Labels in the document. - for elem in introspector.all() { - let Some(label) = elem.label().cloned() else { continue }; - let details = elem - .field("caption") - .or_else(|| elem.field("body")) - .and_then(|field| match field { - Value::Content(content) => Some(content), - _ => None, - }) - .as_ref() - .unwrap_or(elem) - .plain_text(); - output.push((label, Some(details))); - } - - let split = output.len(); - - // Bibliography keys. - for (key, detail) in (items.bibliography_keys)(introspector.track()) { - output.push((Label(key), detail)); - } - - (output, split) -} diff --git a/src/ide/complete.rs b/src/ide/complete.rs deleted file mode 100644 index 16cba1bc..00000000 --- a/src/ide/complete.rs +++ /dev/null @@ -1,1201 +0,0 @@ -use std::collections::{BTreeSet, HashSet}; - -use ecow::{eco_format, EcoString}; -use if_chain::if_chain; -use unscanny::Scanner; - -use super::analyze::analyze_labels; -use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; -use crate::doc::Frame; -use crate::eval::{format_str, methods_on, CastInfo, Library, Scope, Value}; -use crate::syntax::{ - ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, -}; -use crate::util::separated_list; -use crate::World; - -/// Autocomplete a cursor position in a source file. -/// -/// Returns the position from which the completions apply and a list of -/// completions. -/// -/// When `explicit` is `true`, the user requested the completion by pressing -/// control and space or something similar. -pub fn autocomplete( - world: &(dyn World + 'static), - frames: &[Frame], - source: &Source, - cursor: usize, - explicit: bool, -) -> Option<(usize, Vec)> { - let mut ctx = CompletionContext::new(world, frames, source, cursor, explicit)?; - - let _ = complete_comments(&mut ctx) - || complete_field_accesses(&mut ctx) - || complete_imports(&mut ctx) - || complete_rules(&mut ctx) - || complete_params(&mut ctx) - || complete_markup(&mut ctx) - || complete_math(&mut ctx) - || complete_code(&mut ctx); - - Some((ctx.from, ctx.completions)) -} - -/// An autocompletion option. -#[derive(Debug, Clone)] -pub struct Completion { - /// The kind of item this completes to. - pub kind: CompletionKind, - /// The label the completion is shown with. - pub label: EcoString, - /// The completed version of the input, possibly described with snippet - /// syntax like `${lhs} + ${rhs}`. - /// - /// Should default to the `label` if `None`. - pub apply: Option, - /// An optional short description, at most one sentence. - pub detail: Option, -} - -/// A kind of item that can be completed. -#[derive(Debug, Clone)] -pub enum CompletionKind { - /// A syntactical structure. - Syntax, - /// A function. - Func, - /// A function parameter. - Param, - /// A constant. - Constant, - /// A symbol. - Symbol(char), -} - -/// Complete in comments. Or rather, don't! -fn complete_comments(ctx: &mut CompletionContext) -> bool { - matches!(ctx.leaf.kind(), SyntaxKind::LineComment | SyntaxKind::BlockComment) -} - -/// Complete in markup mode. -fn complete_markup(ctx: &mut CompletionContext) -> bool { - // Bail if we aren't even in markup. - if !matches!( - ctx.leaf.parent_kind(), - None | Some(SyntaxKind::Markup) | Some(SyntaxKind::Ref) - ) { - return false; - } - - // Start of an interpolated identifier: "#|". - if ctx.leaf.kind() == SyntaxKind::Hashtag { - ctx.from = ctx.cursor; - code_completions(ctx, true); - return true; - } - - // An existing identifier: "#pa|". - if ctx.leaf.kind() == SyntaxKind::Ident { - ctx.from = ctx.leaf.offset(); - code_completions(ctx, true); - return true; - } - - // Start of an reference: "@|" or "@he|". - if ctx.leaf.kind() == SyntaxKind::RefMarker { - ctx.from = ctx.leaf.offset() + 1; - ctx.label_completions(); - return true; - } - - // Behind a half-completed binding: "#let x = |". - if_chain! { - if let Some(prev) = ctx.leaf.prev_leaf(); - if prev.kind() == SyntaxKind::Eq; - if prev.parent_kind() == Some(SyntaxKind::LetBinding); - then { - ctx.from = ctx.cursor; - code_completions(ctx, false); - return true; - } - } - - // Directly after a raw block. - let mut s = Scanner::new(ctx.text); - s.jump(ctx.leaf.offset()); - if s.eat_if("```") { - s.eat_while('`'); - let start = s.cursor(); - if s.eat_if(is_id_start) { - s.eat_while(is_id_continue); - } - if s.cursor() == ctx.cursor { - ctx.from = start; - ctx.raw_completions(); - } - return true; - } - - // Anywhere: "|". - if ctx.explicit { - ctx.from = ctx.cursor; - markup_completions(ctx); - return true; - } - - false -} - -/// Add completions for markup snippets. -#[rustfmt::skip] -fn markup_completions(ctx: &mut CompletionContext) { - ctx.snippet_completion( - "expression", - "#${}", - "Variables, function calls, blocks, and more.", - ); - - ctx.snippet_completion( - "linebreak", - "\\\n${}", - "Inserts a forced linebreak.", - ); - - ctx.snippet_completion( - "strong text", - "*${strong}*", - "Strongly emphasizes content by increasing the font weight.", - ); - - ctx.snippet_completion( - "emphasized text", - "_${emphasized}_", - "Emphasizes content by setting it in italic font style.", - ); - - ctx.snippet_completion( - "raw text", - "`${text}`", - "Displays text verbatim, in monospace.", - ); - - ctx.snippet_completion( - "code listing", - "```${lang}\n${code}\n```", - "Inserts computer code with syntax highlighting.", - ); - - ctx.snippet_completion( - "hyperlink", - "https://${example.com}", - "Links to a URL.", - ); - - ctx.snippet_completion( - "label", - "<${name}>", - "Makes the preceding element referenceable.", - ); - - ctx.snippet_completion( - "reference", - "@${name}", - "Inserts a reference to a label.", - ); - - ctx.snippet_completion( - "heading", - "= ${title}", - "Inserts a section heading.", - ); - - ctx.snippet_completion( - "list item", - "- ${item}", - "Inserts an item of a bullet list.", - ); - - ctx.snippet_completion( - "enumeration item", - "+ ${item}", - "Inserts an item of a numbered list.", - ); - - ctx.snippet_completion( - "enumeration item (numbered)", - "${number}. ${item}", - "Inserts an explicitly numbered list item.", - ); - - ctx.snippet_completion( - "term list item", - "/ ${term}: ${description}", - "Inserts an item of a term list.", - ); - - ctx.snippet_completion( - "math (inline)", - "$${x}$", - "Inserts an inline-level mathematical equation.", - ); - - ctx.snippet_completion( - "math (block)", - "$ ${sum_x^2} $", - "Inserts a block-level mathematical equation.", - ); -} - -/// Complete in math mode. -fn complete_math(ctx: &mut CompletionContext) -> bool { - if !matches!( - ctx.leaf.parent_kind(), - Some(SyntaxKind::Equation) - | Some(SyntaxKind::Math) - | Some(SyntaxKind::MathFrac) - | Some(SyntaxKind::MathAttach) - ) { - return false; - } - - // Start of an interpolated identifier: "#|". - if ctx.leaf.kind() == SyntaxKind::Hashtag { - ctx.from = ctx.cursor; - code_completions(ctx, true); - return true; - } - - // Behind existing atom or identifier: "$a|$" or "$abc|$". - if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { - ctx.from = ctx.leaf.offset(); - math_completions(ctx); - return true; - } - - // Anywhere: "$|$". - if ctx.explicit { - ctx.from = ctx.cursor; - math_completions(ctx); - return true; - } - - false -} - -/// Add completions for math snippets. -#[rustfmt::skip] -fn math_completions(ctx: &mut CompletionContext) { - ctx.scope_completions(true, |_| true); - - ctx.snippet_completion( - "subscript", - "${x}_${2:2}", - "Sets something in subscript.", - ); - - ctx.snippet_completion( - "superscript", - "${x}^${2:2}", - "Sets something in superscript.", - ); - - ctx.snippet_completion( - "fraction", - "${x}/${y}", - "Inserts a fraction.", - ); -} - -/// Complete field accesses. -fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { - // Behind an expression plus dot: "emoji.|". - if_chain! { - if ctx.leaf.kind() == SyntaxKind::Dot - || (ctx.leaf.kind() == SyntaxKind::Text - && ctx.leaf.text() == "."); - if ctx.leaf.range().end == ctx.cursor; - if let Some(prev) = ctx.leaf.prev_sibling(); - if prev.is::(); - if prev.parent_kind() != Some(SyntaxKind::Markup) || - prev.prev_sibling_kind() == Some(SyntaxKind::Hashtag); - if let Some(value) = analyze_expr(ctx.world, &prev).into_iter().next(); - then { - ctx.from = ctx.cursor; - field_access_completions(ctx, &value); - return true; - } - } - - // Behind a started field access: "emoji.fa|". - if_chain! { - if ctx.leaf.kind() == SyntaxKind::Ident; - if let Some(prev) = ctx.leaf.prev_sibling(); - if prev.kind() == SyntaxKind::Dot; - if let Some(prev_prev) = prev.prev_sibling(); - if prev_prev.is::(); - if let Some(value) = analyze_expr(ctx.world, &prev_prev).into_iter().next(); - then { - ctx.from = ctx.leaf.offset(); - field_access_completions(ctx, &value); - return true; - } - } - - false -} - -/// Add completions for all fields on a value. -fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { - for &(method, args) in methods_on(value.type_name()) { - ctx.completions.push(Completion { - kind: CompletionKind::Func, - label: method.into(), - apply: Some(if args { - eco_format!("{method}(${{}})") - } else { - eco_format!("{method}()${{}}") - }), - detail: None, - }) - } - - match value { - Value::Symbol(symbol) => { - for modifier in symbol.modifiers() { - if let Ok(modified) = symbol.clone().modified(modifier) { - ctx.completions.push(Completion { - kind: CompletionKind::Symbol(modified.get()), - label: modifier.into(), - apply: None, - detail: None, - }); - } - } - } - Value::Content(content) => { - for (name, value) in content.fields() { - ctx.value_completion(Some(name.clone()), &value, false, None); - } - } - Value::Dict(dict) => { - for (name, value) in dict.iter() { - ctx.value_completion(Some(name.clone().into()), value, false, None); - } - } - Value::Module(module) => { - for (name, value) in module.scope().iter() { - ctx.value_completion(Some(name.clone()), value, true, None); - } - } - Value::Func(func) => { - if let Some(info) = func.info() { - // Consider all names from the function's scope. - for (name, value) in info.scope.iter() { - ctx.value_completion(Some(name.clone()), value, true, None); - } - } - } - _ => {} - } -} - -/// Complete imports. -fn complete_imports(ctx: &mut CompletionContext) -> bool { - // In an import path for a package: - // "#import "@|", - if_chain! { - if matches!( - ctx.leaf.parent_kind(), - Some(SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude) - ); - if let Some(ast::Expr::Str(str)) = ctx.leaf.cast(); - if str.get().starts_with('@'); - then { - ctx.from = ctx.leaf.offset(); - ctx.package_completions(); - return true; - } - } - - // Behind an import list: - // "#import "path.typ": |", - // "#import "path.typ": a, b, |". - if_chain! { - if let Some(prev) = ctx.leaf.prev_sibling(); - if let Some(ast::Expr::Import(import)) = prev.cast(); - if let Some(ast::Imports::Items(items)) = import.imports(); - if let Some(source) = prev.children().find(|child| child.is::()); - if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); - then { - ctx.from = ctx.cursor; - import_item_completions(ctx, &items, &value); - return true; - } - } - - // Behind a half-started identifier in an import list: - // "#import "path.typ": thi|", - if_chain! { - if ctx.leaf.kind() == SyntaxKind::Ident; - if let Some(parent) = ctx.leaf.parent(); - if parent.kind() == SyntaxKind::ImportItems; - if let Some(grand) = parent.parent(); - if let Some(ast::Expr::Import(import)) = grand.cast(); - if let Some(ast::Imports::Items(items)) = import.imports(); - if let Some(source) = grand.children().find(|child| child.is::()); - if let Some(value) = analyze_expr(ctx.world, &source).into_iter().next(); - then { - ctx.from = ctx.leaf.offset(); - import_item_completions(ctx, &items, &value); - return true; - } - } - - false -} - -/// Add completions for all exports of a module. -fn import_item_completions( - ctx: &mut CompletionContext, - existing: &[ast::Ident], - value: &Value, -) { - let module = match value { - Value::Str(path) => match analyze_import(ctx.world, ctx.source, path) { - Some(module) => module, - None => return, - }, - Value::Module(module) => module.clone(), - _ => return, - }; - - if existing.is_empty() { - ctx.snippet_completion("*", "*", "Import everything."); - } - - for (name, value) in module.scope().iter() { - if existing.iter().all(|ident| ident.as_str() != name) { - ctx.value_completion(Some(name.clone()), value, false, None); - } - } -} - -/// Complete set and show rules. -fn complete_rules(ctx: &mut CompletionContext) -> bool { - // We don't want to complete directly behind the keyword. - if !ctx.leaf.kind().is_trivia() { - return false; - } - - let Some(prev) = ctx.leaf.prev_leaf() else { return false }; - - // Behind the set keyword: "set |". - if matches!(prev.kind(), SyntaxKind::Set) { - ctx.from = ctx.cursor; - set_rule_completions(ctx); - return true; - } - - // Behind the show keyword: "show |". - if matches!(prev.kind(), SyntaxKind::Show) { - ctx.from = ctx.cursor; - show_rule_selector_completions(ctx); - return true; - } - - // Behind a half-completed show rule: "show strong: |". - if_chain! { - if let Some(prev) = ctx.leaf.prev_leaf(); - if matches!(prev.kind(), SyntaxKind::Colon); - if matches!(prev.parent_kind(), Some(SyntaxKind::ShowRule)); - then { - ctx.from = ctx.cursor; - show_rule_recipe_completions(ctx); - return true; - } - } - - false -} - -/// Add completions for all functions from the global scope. -fn set_rule_completions(ctx: &mut CompletionContext) { - ctx.scope_completions(true, |value| { - matches!( - value, - Value::Func(func) if func.info().map_or(false, |info| { - info.params.iter().any(|param| param.settable) - }), - ) - }); -} - -/// Add completions for selectors. -fn show_rule_selector_completions(ctx: &mut CompletionContext) { - ctx.scope_completions( - false, - |value| matches!(value, Value::Func(func) if func.element().is_some()), - ); - - ctx.enrich("", ": "); - - ctx.snippet_completion( - "text selector", - "\"${text}\": ${}", - "Replace occurrences of specific text.", - ); - - ctx.snippet_completion( - "regex selector", - "regex(\"${regex}\"): ${}", - "Replace matches of a regular expression.", - ); -} - -/// Add completions for recipes. -fn show_rule_recipe_completions(ctx: &mut CompletionContext) { - ctx.snippet_completion( - "replacement", - "[${content}]", - "Replace the selected element with content.", - ); - - ctx.snippet_completion( - "replacement (string)", - "\"${text}\"", - "Replace the selected element with a string of text.", - ); - - ctx.snippet_completion( - "transformation", - "element => [${content}]", - "Transform the element with a function.", - ); - - ctx.scope_completions(false, |value| matches!(value, Value::Func(_))); -} - -/// Complete call and set rule parameters. -fn complete_params(ctx: &mut CompletionContext) -> bool { - // Ensure that we are in a function call or set rule's argument list. - let (callee, set, args) = if_chain! { - if let Some(parent) = ctx.leaf.parent(); - if let Some(parent) = match parent.kind() { - SyntaxKind::Named => parent.parent(), - _ => Some(parent), - }; - if let Some(args) = parent.cast::(); - if let Some(grand) = parent.parent(); - if let Some(expr) = grand.cast::(); - let set = matches!(expr, ast::Expr::Set(_)); - if let Some(ast::Expr::Ident(callee)) = match expr { - ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), - _ => None, - }; - then { - (callee, set, args) - } else { - return false; - } - }; - - // Find the piece of syntax that decides what we're completing. - let mut deciding = ctx.leaf.clone(); - while !matches!( - deciding.kind(), - SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon - ) { - let Some(prev) = deciding.prev_leaf() else { break }; - deciding = prev; - } - - // Parameter values: "func(param:|)", "func(param: |)". - if_chain! { - if deciding.kind() == SyntaxKind::Colon; - if let Some(prev) = deciding.prev_leaf(); - if let Some(param) = prev.cast::(); - then { - if let Some(next) = deciding.next_leaf() { - ctx.from = ctx.cursor.min(next.offset()); - } - - named_param_value_completions(ctx, &callee, ¶m); - return true; - } - } - - // Parameters: "func(|)", "func(hi|)", "func(12,|)". - if_chain! { - if matches!(deciding.kind(), SyntaxKind::LeftParen | SyntaxKind::Comma); - if deciding.kind() != SyntaxKind::Comma || deciding.range().end < ctx.cursor; - then { - if let Some(next) = deciding.next_leaf() { - ctx.from = ctx.cursor.min(next.offset()); - } - - // Exclude arguments which are already present. - let exclude: Vec<_> = args.items().filter_map(|arg| match arg { - ast::Arg::Named(named) => Some(named.name()), - _ => None, - }).collect(); - - param_completions(ctx, &callee, set, &exclude); - return true; - } - } - - false -} - -/// Add completions for the parameters of a function. -fn param_completions( - ctx: &mut CompletionContext, - callee: &ast::Ident, - set: bool, - exclude: &[ast::Ident], -) { - let info = if_chain! { - if let Some(Value::Func(func)) = ctx.global.get(callee); - if let Some(info) = func.info(); - then { info } - else { return; } - }; - - for param in &info.params { - if exclude.iter().any(|ident| ident.as_str() == param.name) { - continue; - } - - if set && !param.settable { - continue; - } - - if param.named { - ctx.completions.push(Completion { - kind: CompletionKind::Param, - label: param.name.into(), - apply: Some(eco_format!("{}: ${{}}", param.name)), - detail: Some(plain_docs_sentence(param.docs)), - }); - } - - if param.positional { - ctx.cast_completions(¶m.cast); - } - } - - if ctx.before.ends_with(',') { - ctx.enrich(" ", ""); - } -} - -/// Add completions for the values of a named function parameter. -fn named_param_value_completions( - ctx: &mut CompletionContext, - callee: &ast::Ident, - name: &str, -) { - let param = if_chain! { - if let Some(Value::Func(func)) = ctx.global.get(callee); - if let Some(info) = func.info(); - if let Some(param) = info.param(name); - if param.named; - then { param } - else { return; } - }; - - ctx.cast_completions(¶m.cast); - - if callee.as_str() == "text" && name == "font" { - ctx.font_completions(); - } - - if ctx.before.ends_with(':') { - ctx.enrich(" ", ""); - } -} - -/// Complete in code mode. -fn complete_code(ctx: &mut CompletionContext) -> bool { - if matches!( - ctx.leaf.parent_kind(), - None | Some(SyntaxKind::Markup) - | Some(SyntaxKind::Math) - | Some(SyntaxKind::MathFrac) - | Some(SyntaxKind::MathAttach) - | Some(SyntaxKind::MathRoot) - ) { - return false; - } - - // An existing identifier: "{ pa| }". - if ctx.leaf.kind() == SyntaxKind::Ident { - ctx.from = ctx.leaf.offset(); - code_completions(ctx, false); - return true; - } - - // Anywhere: "{ | }". - // But not within or after an expression. - if ctx.explicit - && (ctx.leaf.kind().is_trivia() - || matches!(ctx.leaf.kind(), SyntaxKind::LeftParen | SyntaxKind::LeftBrace)) - { - ctx.from = ctx.cursor; - code_completions(ctx, false); - return true; - } - - false -} - -/// Add completions for expression snippets. -#[rustfmt::skip] -fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { - ctx.scope_completions(true, |value| !hashtag || { - matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Module(_)) - }); - - ctx.snippet_completion( - "function call", - "${function}(${arguments})[${body}]", - "Evaluates a function.", - ); - - ctx.snippet_completion( - "code block", - "{ ${} }", - "Inserts a nested code block.", - ); - - ctx.snippet_completion( - "content block", - "[${content}]", - "Switches into markup mode.", - ); - - ctx.snippet_completion( - "set rule", - "set ${}", - "Sets style properties on an element.", - ); - - ctx.snippet_completion( - "show rule", - "show ${}", - "Redefines the look of an element.", - ); - - ctx.snippet_completion( - "show rule (everything)", - "show: ${}", - "Transforms everything that follows.", - ); - - ctx.snippet_completion( - "let binding", - "let ${name} = ${value}", - "Saves a value in a variable.", - ); - - ctx.snippet_completion( - "let binding (function)", - "let ${name}(${params}) = ${output}", - "Defines a function.", - ); - - ctx.snippet_completion( - "if conditional", - "if ${1 < 2} {\n\t${}\n}", - "Computes or inserts something conditionally.", - ); - - ctx.snippet_completion( - "if-else conditional", - "if ${1 < 2} {\n\t${}\n} else {\n\t${}\n}", - "Computes or inserts different things based on a condition.", - ); - - ctx.snippet_completion( - "while loop", - "while ${1 < 2} {\n\t${}\n}", - "Computes or inserts something while a condition is met.", - ); - - ctx.snippet_completion( - "for loop", - "for ${value} in ${(1, 2, 3)} {\n\t${}\n}", - "Computes or inserts something for each value in a collection.", - ); - - ctx.snippet_completion( - "for loop (with key)", - "for ${key}, ${value} in ${(a: 1, b: 2)} {\n\t${}\n}", - "Computes or inserts something for each key and value in a collection.", - ); - - ctx.snippet_completion( - "break", - "break", - "Exits early from a loop.", - ); - - ctx.snippet_completion( - "continue", - "continue", - "Continues with the next iteration of a loop.", - ); - - ctx.snippet_completion( - "return", - "return ${output}", - "Returns early from a function.", - ); - - ctx.snippet_completion( - "import (file)", - "import \"${file}.typ\": ${items}", - "Imports variables from another file.", - ); - - ctx.snippet_completion( - "import (package)", - "import \"@${}\": ${items}", - "Imports variables from another file.", - ); - - ctx.snippet_completion( - "include (file)", - "include \"${file}.typ\"", - "Includes content from another file.", - ); - - ctx.snippet_completion( - "include (package)", - "include \"@${}\"", - "Includes content from another file.", - ); - - ctx.snippet_completion( - "array", - "(${1, 2, 3})", - "Creates a sequence of values.", - ); - - ctx.snippet_completion( - "dictionary", - "(${a: 1, b: 2})", - "Creates a mapping from names to value.", - ); - - if !hashtag { - ctx.snippet_completion( - "function", - "(${params}) => ${output}", - "Creates an unnamed function.", - ); - } -} - -/// Context for autocompletion. -struct CompletionContext<'a> { - world: &'a (dyn World + 'static), - frames: &'a [Frame], - library: &'a Library, - source: &'a Source, - global: &'a Scope, - math: &'a Scope, - text: &'a str, - before: &'a str, - after: &'a str, - leaf: LinkedNode<'a>, - cursor: usize, - explicit: bool, - from: usize, - completions: Vec, - seen_casts: HashSet, -} - -impl<'a> CompletionContext<'a> { - /// Create a new autocompletion context. - fn new( - world: &'a (dyn World + 'static), - frames: &'a [Frame], - source: &'a Source, - cursor: usize, - explicit: bool, - ) -> Option { - let text = source.text(); - let library = world.library(); - let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; - Some(Self { - world, - frames, - library, - source, - global: library.global.scope(), - math: library.math.scope(), - text, - before: &text[..cursor], - after: &text[cursor..], - leaf, - cursor, - explicit, - from: cursor, - completions: vec![], - seen_casts: HashSet::new(), - }) - } - - /// Add a prefix and suffix to all applications. - fn enrich(&mut self, prefix: &str, suffix: &str) { - for Completion { label, apply, .. } in &mut self.completions { - let current = apply.as_ref().unwrap_or(label); - *apply = Some(eco_format!("{prefix}{current}{suffix}")); - } - } - - /// Add a snippet completion. - fn snippet_completion( - &mut self, - label: &'static str, - snippet: &'static str, - docs: &'static str, - ) { - self.completions.push(Completion { - kind: CompletionKind::Syntax, - label: label.into(), - apply: Some(snippet.into()), - detail: Some(docs.into()), - }); - } - - /// Add completions for all font families. - fn font_completions(&mut self) { - let equation = self.before[self.cursor.saturating_sub(25)..].contains("equation"); - for (family, iter) in self.world.book().families() { - let detail = summarize_font_family(iter); - if !equation || family.contains("Math") { - self.value_completion( - None, - &Value::Str(family.into()), - false, - Some(detail.as_str()), - ); - } - } - } - - /// Add completions for all available packages. - fn package_completions(&mut self) { - for (package, description) in self.world.packages() { - self.value_completion( - None, - &Value::Str(format_str!("{package}")), - false, - description.as_deref(), - ); - } - } - - /// Add completions for raw block tags. - fn raw_completions(&mut self) { - for (name, mut tags) in (self.library.items.raw_languages)() { - let lower = name.to_lowercase(); - if !tags.contains(&lower.as_str()) { - tags.push(lower.as_str()); - } - - tags.retain(|tag| is_ident(tag)); - if tags.is_empty() { - continue; - } - - self.completions.push(Completion { - kind: CompletionKind::Constant, - label: name.into(), - apply: Some(tags[0].into()), - detail: Some(separated_list(&tags, " or ").into()), - }); - } - } - - /// Add completions for all labels. - fn label_completions(&mut self) { - for (label, detail) in analyze_labels(self.world, self.frames).0 { - self.completions.push(Completion { - kind: CompletionKind::Constant, - label: label.0, - apply: None, - detail, - }); - } - } - - /// Add a completion for a specific value. - fn value_completion( - &mut self, - label: Option, - value: &Value, - parens: bool, - docs: Option<&str>, - ) { - let label = label.unwrap_or_else(|| value.repr().into()); - let mut apply = None; - - if label.starts_with('"') && self.after.starts_with('"') { - if let Some(trimmed) = label.strip_suffix('"') { - apply = Some(trimmed.into()); - } - } - - let detail = docs.map(Into::into).or_else(|| match value { - Value::Symbol(_) => None, - Value::Func(func) => func.info().map(|info| plain_docs_sentence(info.docs)), - v => { - let repr = v.repr(); - (repr.as_str() != label).then(|| repr.into()) - } - }); - - if parens && matches!(value, Value::Func(_)) { - apply = Some(eco_format!("{label}(${{}})")); - } - - self.completions.push(Completion { - kind: match value { - Value::Func(_) => CompletionKind::Func, - Value::Symbol(s) => CompletionKind::Symbol(s.get()), - _ => CompletionKind::Constant, - }, - label, - apply, - detail, - }); - } - - /// Add completions for a castable. - fn cast_completions(&mut self, cast: &'a CastInfo) { - // Prevent duplicate completions from appearing. - if !self.seen_casts.insert(crate::util::hash128(cast)) { - return; - } - - match cast { - CastInfo::Any => {} - CastInfo::Value(value, docs) => { - self.value_completion(None, value, true, Some(docs)); - } - CastInfo::Type("none") => self.snippet_completion("none", "none", "Nothing."), - CastInfo::Type("auto") => { - self.snippet_completion("auto", "auto", "A smart default."); - } - CastInfo::Type("boolean") => { - self.snippet_completion("false", "false", "No / Disabled."); - self.snippet_completion("true", "true", "Yes / Enabled."); - } - CastInfo::Type("color") => { - self.snippet_completion( - "luma()", - "luma(${v})", - "A custom grayscale color.", - ); - self.snippet_completion( - "rgb()", - "rgb(${r}, ${g}, ${b}, ${a})", - "A custom RGBA color.", - ); - self.snippet_completion( - "cmyk()", - "cmyk(${c}, ${m}, ${y}, ${k})", - "A custom CMYK color.", - ); - self.scope_completions(false, |value| value.type_name() == "color"); - } - CastInfo::Type("function") => { - self.snippet_completion( - "function", - "(${params}) => ${output}", - "A custom function.", - ); - } - CastInfo::Type(ty) => { - self.completions.push(Completion { - kind: CompletionKind::Syntax, - label: (*ty).into(), - apply: Some(eco_format!("${{{ty}}}")), - detail: Some(eco_format!("A value of type {ty}.")), - }); - self.scope_completions(false, |value| value.type_name() == *ty); - } - CastInfo::Union(union) => { - for info in union { - self.cast_completions(info); - } - } - } - } - - /// Add completions for definitions that are available at the cursor. - /// Filters the global/math scope with the given filter. - fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) { - let mut defined = BTreeSet::new(); - - let mut ancestor = Some(self.leaf.clone()); - while let Some(node) = &ancestor { - let mut sibling = Some(node.clone()); - while let Some(node) = &sibling { - if let Some(v) = node.cast::() { - for ident in v.kind().idents() { - defined.insert(ident.take()); - } - } - sibling = node.prev_sibling(); - } - - if let Some(parent) = node.parent() { - if let Some(v) = parent.cast::() { - if node.prev_sibling_kind() != Some(SyntaxKind::In) { - let pattern = v.pattern(); - for ident in pattern.idents() { - defined.insert(ident.take()); - } - } - } - - ancestor = Some(parent.clone()); - continue; - } - - break; - } - - let in_math = matches!( - self.leaf.parent_kind(), - Some(SyntaxKind::Equation) - | Some(SyntaxKind::Math) - | Some(SyntaxKind::MathFrac) - | Some(SyntaxKind::MathAttach) - ); - - let scope = if in_math { self.math } else { self.global }; - for (name, value) in scope.iter() { - if filter(value) && !defined.contains(name) { - self.value_completion(Some(name.clone()), value, parens, None); - } - } - - for name in defined { - if !name.is_empty() { - self.completions.push(Completion { - kind: CompletionKind::Constant, - label: name, - apply: None, - detail: None, - }); - } - } - } -} diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs deleted file mode 100644 index 2db636e3..00000000 --- a/src/ide/highlight.rs +++ /dev/null @@ -1,430 +0,0 @@ -use crate::syntax::{ast, LinkedNode, SyntaxKind, SyntaxNode}; - -/// A syntax highlighting tag. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Tag { - /// A line or block comment. - Comment, - /// Punctuation in code. - Punctuation, - /// An escape sequence or shorthand. - Escape, - /// Strong markup. - Strong, - /// Emphasized markup. - Emph, - /// A hyperlink. - Link, - /// Raw text. - Raw, - /// A label. - Label, - /// A reference to a label. - Ref, - /// A section heading. - Heading, - /// A marker of a list, enumeration, or term list. - ListMarker, - /// A term in a term list. - ListTerm, - /// The delimiters of an equation. - MathDelimiter, - /// An operator with special meaning in an equation. - MathOperator, - /// A keyword. - Keyword, - /// An operator in code. - Operator, - /// A numeric literal. - Number, - /// A string literal. - String, - /// A function or method name. - Function, - /// An interpolated variable in markup or math. - Interpolated, - /// A syntax error. - Error, -} - -impl Tag { - /// Return the recommended TextMate grammar scope for the given highlighting - /// tag. - pub fn tm_scope(&self) -> &'static str { - match self { - Self::Comment => "comment.typst", - Self::Punctuation => "punctuation.typst", - Self::Escape => "constant.character.escape.typst", - Self::Strong => "markup.bold.typst", - Self::Emph => "markup.italic.typst", - Self::Link => "markup.underline.link.typst", - Self::Raw => "markup.raw.typst", - Self::MathDelimiter => "punctuation.definition.math.typst", - Self::MathOperator => "keyword.operator.math.typst", - Self::Heading => "markup.heading.typst", - Self::ListMarker => "punctuation.definition.list.typst", - Self::ListTerm => "markup.list.term.typst", - Self::Label => "entity.name.label.typst", - Self::Ref => "markup.other.reference.typst", - Self::Keyword => "keyword.typst", - Self::Operator => "keyword.operator.typst", - Self::Number => "constant.numeric.typst", - Self::String => "string.quoted.double.typst", - Self::Function => "entity.name.function.typst", - Self::Interpolated => "meta.interpolation.typst", - Self::Error => "invalid.typst", - } - } - - /// The recommended CSS class for the highlighting tag. - pub fn css_class(self) -> &'static str { - match self { - Self::Comment => "typ-comment", - Self::Punctuation => "typ-punct", - Self::Escape => "typ-escape", - Self::Strong => "typ-strong", - Self::Emph => "typ-emph", - Self::Link => "typ-link", - Self::Raw => "typ-raw", - Self::Label => "typ-label", - Self::Ref => "typ-ref", - Self::Heading => "typ-heading", - Self::ListMarker => "typ-marker", - Self::ListTerm => "typ-term", - Self::MathDelimiter => "typ-math-delim", - Self::MathOperator => "typ-math-op", - Self::Keyword => "typ-key", - Self::Operator => "typ-op", - Self::Number => "typ-num", - Self::String => "typ-str", - Self::Function => "typ-func", - Self::Interpolated => "typ-pol", - Self::Error => "typ-error", - } - } -} - -/// Determine the highlight tag of a linked syntax node. -/// -/// Returns `None` if the node should not be highlighted. -pub fn highlight(node: &LinkedNode) -> Option { - match node.kind() { - SyntaxKind::Markup - if node.parent_kind() == Some(SyntaxKind::TermItem) - && node.next_sibling_kind() == Some(SyntaxKind::Colon) => - { - Some(Tag::ListTerm) - } - SyntaxKind::Markup => None, - SyntaxKind::Text => None, - SyntaxKind::Space => None, - SyntaxKind::Linebreak => Some(Tag::Escape), - SyntaxKind::Parbreak => None, - SyntaxKind::Escape => Some(Tag::Escape), - SyntaxKind::Shorthand => Some(Tag::Escape), - SyntaxKind::SmartQuote => None, - SyntaxKind::Strong => Some(Tag::Strong), - SyntaxKind::Emph => Some(Tag::Emph), - SyntaxKind::Raw => Some(Tag::Raw), - SyntaxKind::Link => Some(Tag::Link), - SyntaxKind::Label => Some(Tag::Label), - SyntaxKind::Ref => Some(Tag::Ref), - SyntaxKind::RefMarker => None, - SyntaxKind::Heading => Some(Tag::Heading), - SyntaxKind::HeadingMarker => None, - SyntaxKind::ListItem => None, - SyntaxKind::ListMarker => Some(Tag::ListMarker), - SyntaxKind::EnumItem => None, - SyntaxKind::EnumMarker => Some(Tag::ListMarker), - SyntaxKind::TermItem => None, - SyntaxKind::TermMarker => Some(Tag::ListMarker), - SyntaxKind::Equation => None, - - SyntaxKind::Math => None, - SyntaxKind::MathIdent => highlight_ident(node), - SyntaxKind::MathAlignPoint => Some(Tag::MathOperator), - SyntaxKind::MathDelimited => None, - SyntaxKind::MathAttach => None, - SyntaxKind::MathFrac => None, - SyntaxKind::MathRoot => None, - - SyntaxKind::Hashtag => highlight_hashtag(node), - SyntaxKind::LeftBrace => Some(Tag::Punctuation), - SyntaxKind::RightBrace => Some(Tag::Punctuation), - SyntaxKind::LeftBracket => Some(Tag::Punctuation), - SyntaxKind::RightBracket => Some(Tag::Punctuation), - SyntaxKind::LeftParen => Some(Tag::Punctuation), - SyntaxKind::RightParen => Some(Tag::Punctuation), - SyntaxKind::Comma => Some(Tag::Punctuation), - SyntaxKind::Semicolon => Some(Tag::Punctuation), - SyntaxKind::Colon => Some(Tag::Punctuation), - SyntaxKind::Star => match node.parent_kind() { - Some(SyntaxKind::Strong) => None, - _ => Some(Tag::Operator), - }, - SyntaxKind::Underscore => match node.parent_kind() { - Some(SyntaxKind::MathAttach) => Some(Tag::MathOperator), - _ => None, - }, - SyntaxKind::Dollar => Some(Tag::MathDelimiter), - SyntaxKind::Plus => Some(Tag::Operator), - SyntaxKind::Minus => Some(Tag::Operator), - SyntaxKind::Slash => Some(match node.parent_kind() { - Some(SyntaxKind::MathFrac) => Tag::MathOperator, - _ => Tag::Operator, - }), - SyntaxKind::Hat => Some(Tag::MathOperator), - SyntaxKind::Dot => Some(Tag::Punctuation), - SyntaxKind::Eq => match node.parent_kind() { - Some(SyntaxKind::Heading) => None, - _ => Some(Tag::Operator), - }, - SyntaxKind::EqEq => Some(Tag::Operator), - SyntaxKind::ExclEq => Some(Tag::Operator), - SyntaxKind::Lt => Some(Tag::Operator), - SyntaxKind::LtEq => Some(Tag::Operator), - SyntaxKind::Gt => Some(Tag::Operator), - SyntaxKind::GtEq => Some(Tag::Operator), - SyntaxKind::PlusEq => Some(Tag::Operator), - SyntaxKind::HyphEq => Some(Tag::Operator), - SyntaxKind::StarEq => Some(Tag::Operator), - SyntaxKind::SlashEq => Some(Tag::Operator), - SyntaxKind::Dots => Some(Tag::Operator), - SyntaxKind::Arrow => Some(Tag::Operator), - SyntaxKind::Root => Some(Tag::MathOperator), - - SyntaxKind::Not => Some(Tag::Keyword), - SyntaxKind::And => Some(Tag::Keyword), - SyntaxKind::Or => Some(Tag::Keyword), - SyntaxKind::None => Some(Tag::Keyword), - SyntaxKind::Auto => Some(Tag::Keyword), - SyntaxKind::Let => Some(Tag::Keyword), - SyntaxKind::Set => Some(Tag::Keyword), - SyntaxKind::Show => Some(Tag::Keyword), - SyntaxKind::If => Some(Tag::Keyword), - SyntaxKind::Else => Some(Tag::Keyword), - SyntaxKind::For => Some(Tag::Keyword), - SyntaxKind::In => Some(Tag::Keyword), - SyntaxKind::While => Some(Tag::Keyword), - SyntaxKind::Break => Some(Tag::Keyword), - SyntaxKind::Continue => Some(Tag::Keyword), - SyntaxKind::Return => Some(Tag::Keyword), - SyntaxKind::Import => Some(Tag::Keyword), - SyntaxKind::Include => Some(Tag::Keyword), - SyntaxKind::As => Some(Tag::Keyword), - - SyntaxKind::Code => None, - SyntaxKind::Ident => highlight_ident(node), - SyntaxKind::Bool => Some(Tag::Keyword), - SyntaxKind::Int => Some(Tag::Number), - SyntaxKind::Float => Some(Tag::Number), - SyntaxKind::Numeric => Some(Tag::Number), - SyntaxKind::Str => Some(Tag::String), - SyntaxKind::CodeBlock => None, - SyntaxKind::ContentBlock => None, - SyntaxKind::Parenthesized => None, - SyntaxKind::Array => None, - SyntaxKind::Dict => None, - SyntaxKind::Named => None, - SyntaxKind::Keyed => None, - SyntaxKind::Unary => None, - SyntaxKind::Binary => None, - SyntaxKind::FieldAccess => None, - SyntaxKind::FuncCall => None, - SyntaxKind::Args => None, - SyntaxKind::Spread => None, - SyntaxKind::Closure => None, - SyntaxKind::Params => None, - SyntaxKind::LetBinding => None, - SyntaxKind::SetRule => None, - SyntaxKind::ShowRule => None, - SyntaxKind::Conditional => None, - SyntaxKind::WhileLoop => None, - SyntaxKind::ForLoop => None, - SyntaxKind::ModuleImport => None, - SyntaxKind::ImportItems => None, - SyntaxKind::ModuleInclude => None, - SyntaxKind::LoopBreak => None, - SyntaxKind::LoopContinue => None, - SyntaxKind::FuncReturn => None, - SyntaxKind::Destructuring => None, - SyntaxKind::DestructAssignment => None, - - SyntaxKind::LineComment => Some(Tag::Comment), - SyntaxKind::BlockComment => Some(Tag::Comment), - SyntaxKind::Error => Some(Tag::Error), - SyntaxKind::Eof => None, - } -} - -/// Highlight an identifier based on context. -fn highlight_ident(node: &LinkedNode) -> Option { - // Are we directly before an argument list? - let next_leaf = node.next_leaf(); - if let Some(next) = &next_leaf { - if node.range().end == next.offset() - && ((next.kind() == SyntaxKind::LeftParen - && matches!( - next.parent_kind(), - Some(SyntaxKind::Args | SyntaxKind::Params) - )) - || (next.kind() == SyntaxKind::LeftBracket - && next.parent_kind() == Some(SyntaxKind::ContentBlock))) - { - return Some(Tag::Function); - } - } - - // Are we in math? - if node.kind() == SyntaxKind::MathIdent { - return Some(Tag::Interpolated); - } - - // Find the first non-field access ancestor. - let mut ancestor = node; - while ancestor.parent_kind() == Some(SyntaxKind::FieldAccess) { - ancestor = ancestor.parent()?; - } - - // Are we directly before or behind a show rule colon? - if ancestor.parent_kind() == Some(SyntaxKind::ShowRule) - && (next_leaf.map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon) - || node.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Colon)) - { - return Some(Tag::Function); - } - - // Are we (or an ancestor field access) directly after a hashtag. - if ancestor.prev_leaf().map(|leaf| leaf.kind()) == Some(SyntaxKind::Hashtag) { - return Some(Tag::Interpolated); - } - - // Are we behind a dot, that is behind another identifier? - let prev = node.prev_leaf()?; - if prev.kind() == SyntaxKind::Dot { - let prev_prev = prev.prev_leaf()?; - if is_ident(&prev_prev) { - return highlight_ident(&prev_prev); - } - } - - None -} - -/// Highlight a hashtag based on context. -fn highlight_hashtag(node: &LinkedNode) -> Option { - let next = node.next_sibling()?; - let expr = next.cast::()?; - if !expr.hashtag() { - return None; - } - highlight(&next.leftmost_leaf()?) -} - -/// Whether the node is one of the two identifier nodes. -fn is_ident(node: &LinkedNode) -> bool { - matches!(node.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent) -} - -/// Highlight a node to an HTML `code` element. -/// -/// This uses these [CSS classes for categories](Tag::css_class). -pub fn highlight_html(root: &SyntaxNode) -> String { - let mut buf = String::from(""); - let node = LinkedNode::new(root); - highlight_html_impl(&mut buf, &node); - buf.push_str(""); - buf -} - -/// Highlight one source node, emitting HTML. -fn highlight_html_impl(html: &mut String, node: &LinkedNode) { - let mut span = false; - if let Some(tag) = highlight(node) { - if tag != Tag::Error { - span = true; - html.push_str(""); - } - } - - let text = node.text(); - if !text.is_empty() { - for c in text.chars() { - match c { - '<' => html.push_str("<"), - '>' => html.push_str(">"), - '&' => html.push_str("&"), - '\'' => html.push_str("'"), - '"' => html.push_str("""), - _ => html.push(c), - } - } - } else { - for child in node.children() { - highlight_html_impl(html, &child); - } - } - - if span { - html.push_str(""); - } -} - -#[cfg(test)] -mod tests { - use std::ops::Range; - - use super::*; - use crate::syntax::Source; - - #[test] - fn test_highlighting() { - use Tag::*; - - #[track_caller] - fn test(text: &str, goal: &[(Range, Tag)]) { - let mut vec = vec![]; - let source = Source::detached(text); - highlight_tree(&mut vec, &LinkedNode::new(source.root())); - assert_eq!(vec, goal); - } - - fn highlight_tree(tags: &mut Vec<(Range, Tag)>, node: &LinkedNode) { - if let Some(tag) = highlight(node) { - tags.push((node.range(), tag)); - } - - for child in node.children() { - highlight_tree(tags, &child); - } - } - - test("= *AB*", &[(0..6, Heading), (2..6, Strong)]); - - test( - "#f(x + 1)", - &[ - (0..1, Function), - (1..2, Function), - (2..3, Punctuation), - (5..6, Operator), - (7..8, Number), - (8..9, Punctuation), - ], - ); - - test( - "#let f(x) = x", - &[ - (0..1, Keyword), - (1..4, Keyword), - (5..6, Function), - (6..7, Punctuation), - (8..9, Punctuation), - (10..11, Operator), - ], - ); - } -} diff --git a/src/ide/jump.rs b/src/ide/jump.rs deleted file mode 100644 index 14a82e26..00000000 --- a/src/ide/jump.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::num::NonZeroUsize; - -use ecow::EcoString; - -use crate::doc::{Destination, Frame, FrameItem, Meta, Position}; -use crate::file::FileId; -use crate::geom::{Geometry, Point, Size}; -use crate::model::Introspector; -use crate::syntax::{LinkedNode, Source, Span, SyntaxKind}; -use crate::World; - -/// Where to [jump](jump_from_click) to. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum Jump { - /// Jump to a position in a source file. - Source(FileId, usize), - /// Jump to an external URL. - Url(EcoString), - /// Jump to a point on a page. - Position(Position), -} - -impl Jump { - fn from_span(world: &dyn World, span: Span) -> Option { - let source = world.source(span.id()).ok()?; - let node = source.find(span)?; - Some(Self::Source(span.id(), node.offset())) - } -} - -/// Determine where to jump to based on a click in a frame. -pub fn jump_from_click( - world: &dyn World, - frames: &[Frame], - frame: &Frame, - click: Point, -) -> Option { - let mut introspector = None; - - // Try to find a link first. - for (pos, item) in frame.items() { - if let FrameItem::Meta(Meta::Link(dest), size) = item { - if is_in_rect(*pos, *size, click) { - return Some(match dest { - Destination::Url(url) => Jump::Url(url.clone()), - Destination::Position(pos) => Jump::Position(*pos), - Destination::Location(loc) => Jump::Position( - introspector - .get_or_insert_with(|| Introspector::new(frames)) - .position(*loc), - ), - }); - } - } - } - - // If there's no link, search for a jump target. - for (mut pos, item) in frame.items().rev() { - match item { - FrameItem::Group(group) => { - // TODO: Handle transformation. - if let Some(span) = - jump_from_click(world, frames, &group.frame, click - pos) - { - return Some(span); - } - } - - FrameItem::Text(text) => { - for glyph in &text.glyphs { - let (span, span_offset) = glyph.span; - if span.is_detached() { - continue; - } - - let width = glyph.x_advance.at(text.size); - if is_in_rect( - Point::new(pos.x, pos.y - text.size), - Size::new(width, text.size), - click, - ) { - let source = world.source(span.id()).ok()?; - let node = source.find(span)?; - let pos = if node.kind() == SyntaxKind::Text { - let range = node.range(); - let mut offset = range.start + usize::from(span_offset); - if (click.x - pos.x) > width / 2.0 { - offset += glyph.range().len(); - } - offset.min(range.end) - } else { - node.offset() - }; - return Some(Jump::Source(source.id(), pos)); - } - - pos.x += width; - } - } - - FrameItem::Shape(shape, span) => { - let Geometry::Rect(size) = shape.geometry else { continue }; - if is_in_rect(pos, size, click) { - return Jump::from_span(world, *span); - } - } - - FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => { - return Jump::from_span(world, *span); - } - - _ => {} - } - } - - None -} - -/// Find the output location in the document for a cursor position. -pub fn jump_from_cursor( - frames: &[Frame], - source: &Source, - cursor: usize, -) -> Option { - let node = LinkedNode::new(source.root()).leaf_at(cursor)?; - if node.kind() != SyntaxKind::Text { - return None; - } - - let span = node.span(); - for (i, frame) in frames.iter().enumerate() { - if let Some(pos) = find_in_frame(frame, span) { - return Some(Position { - page: NonZeroUsize::new(i + 1).unwrap(), - point: pos, - }); - } - } - - None -} - -/// Find the position of a span in a frame. -fn find_in_frame(frame: &Frame, span: Span) -> Option { - for (mut pos, item) in frame.items() { - if let FrameItem::Group(group) = item { - // TODO: Handle transformation. - if let Some(point) = find_in_frame(&group.frame, span) { - return Some(point + pos); - } - } - - if let FrameItem::Text(text) = item { - for glyph in &text.glyphs { - if glyph.span.0 == span { - return Some(pos); - } - pos.x += glyph.x_advance.at(text.size); - } - } - } - - None -} - -/// Whether a rectangle with the given size at the given position contains the -/// click position. -fn is_in_rect(pos: Point, size: Size, click: Point) -> bool { - pos.x <= click.x - && pos.x + size.x >= click.x - && pos.y <= click.y - && pos.y + size.y >= click.y -} diff --git a/src/ide/mod.rs b/src/ide/mod.rs deleted file mode 100644 index 4b08b66b..00000000 --- a/src/ide/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Capabilities for IDE support. - -mod analyze; -mod complete; -mod highlight; -mod jump; -mod tooltip; - -pub use self::analyze::analyze_labels; -pub use self::complete::{autocomplete, Completion, CompletionKind}; -pub use self::highlight::{highlight, highlight_html, Tag}; -pub use self::jump::{jump_from_click, jump_from_cursor, Jump}; -pub use self::tooltip::{tooltip, Tooltip}; - -use std::fmt::Write; - -use ecow::{eco_format, EcoString}; - -use self::analyze::*; -use crate::font::{FontInfo, FontStyle}; - -/// Extract the first sentence of plain text of a piece of documentation. -/// -/// Removes Markdown formatting. -fn plain_docs_sentence(docs: &str) -> EcoString { - let mut s = unscanny::Scanner::new(docs); - let mut output = EcoString::new(); - let mut link = false; - while let Some(c) = s.eat() { - match c { - '`' => { - let mut raw = s.eat_until('`'); - if (raw.starts_with('{') && raw.ends_with('}')) - || (raw.starts_with('[') && raw.ends_with(']')) - { - raw = &raw[1..raw.len() - 1]; - } - - s.eat(); - output.push('`'); - output.push_str(raw); - output.push('`'); - } - '[' => link = true, - ']' if link => { - if s.eat_if('(') { - s.eat_until(')'); - s.eat(); - } else if s.eat_if('[') { - s.eat_until(']'); - s.eat(); - } - link = false - } - '*' | '_' => {} - '.' => { - output.push('.'); - break; - } - _ => output.push(c), - } - } - - output -} - -/// Create a short description of a font family. -fn summarize_font_family<'a>(variants: impl Iterator) -> EcoString { - let mut infos: Vec<_> = variants.collect(); - infos.sort_by_key(|info| info.variant); - - let mut has_italic = false; - let mut min_weight = u16::MAX; - let mut max_weight = 0; - for info in &infos { - let weight = info.variant.weight.to_number(); - has_italic |= info.variant.style == FontStyle::Italic; - min_weight = min_weight.min(weight); - max_weight = min_weight.max(weight); - } - - let count = infos.len(); - let s = if count == 1 { "" } else { "s" }; - let mut detail = eco_format!("{count} variant{s}."); - - if min_weight == max_weight { - write!(detail, " Weight {min_weight}.").unwrap(); - } else { - write!(detail, " Weights {min_weight}–{max_weight}.").unwrap(); - } - - if has_italic { - detail.push_str(" Has italics."); - } - - detail -} diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs deleted file mode 100644 index 35125e92..00000000 --- a/src/ide/tooltip.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::fmt::Write; - -use ecow::{eco_format, EcoString}; - -use if_chain::if_chain; - -use super::analyze::analyze_labels; -use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; -use crate::doc::Frame; -use crate::eval::{CastInfo, Tracer, Value}; -use crate::geom::{round_2, Length, Numeric}; -use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; -use crate::util::pretty_comma_list; -use crate::World; - -/// Describe the item under the cursor. -pub fn tooltip( - world: &(dyn World + 'static), - frames: &[Frame], - source: &Source, - cursor: usize, -) -> Option { - let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; - if leaf.kind().is_trivia() { - return None; - } - - named_param_tooltip(world, &leaf) - .or_else(|| font_tooltip(world, &leaf)) - .or_else(|| ref_tooltip(world, frames, &leaf)) - .or_else(|| expr_tooltip(world, &leaf)) -} - -/// A hover tooltip. -#[derive(Debug, Clone)] -pub enum Tooltip { - /// A string of text. - Text(EcoString), - /// A string of Typst code. - Code(EcoString), -} - -/// Tooltip for a hovered expression. -fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option { - let mut ancestor = leaf; - while !ancestor.is::() { - ancestor = ancestor.parent()?; - } - - let expr = ancestor.cast::()?; - if !expr.hashtag() && !matches!(expr, ast::Expr::MathIdent(_)) { - return None; - } - - let values = analyze_expr(world, ancestor); - - if let [value] = values.as_slice() { - if let Some(docs) = value.docs() { - return Some(Tooltip::Text(plain_docs_sentence(docs))); - } - - if let &Value::Length(length) = value { - if let Some(tooltip) = length_tooltip(length) { - return Some(tooltip); - } - } - } - - if expr.is_literal() { - return None; - } - - let mut last = None; - let mut pieces: Vec = vec![]; - let mut iter = values.iter(); - for value in (&mut iter).take(Tracer::MAX - 1) { - if let Some((prev, count)) = &mut last { - if *prev == value { - *count += 1; - continue; - } else if *count > 1 { - write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); - } - } - pieces.push(value.repr().into()); - last = Some((value, 1)); - } - - if let Some((_, count)) = last { - if count > 1 { - write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); - } - } - - if iter.next().is_some() { - pieces.push("...".into()); - } - - let tooltip = pretty_comma_list(&pieces, false); - (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into())) -} - -/// Tooltip text for a hovered length. -fn length_tooltip(length: Length) -> Option { - length.em.is_zero().then(|| { - Tooltip::Code(eco_format!( - "{}pt = {}mm = {}cm = {}in", - round_2(length.abs.to_pt()), - round_2(length.abs.to_mm()), - round_2(length.abs.to_cm()), - round_2(length.abs.to_inches()) - )) - }) -} - -/// Tooltip for a hovered reference. -fn ref_tooltip( - world: &(dyn World + 'static), - frames: &[Frame], - leaf: &LinkedNode, -) -> Option { - if leaf.kind() != SyntaxKind::RefMarker { - return None; - } - - let target = leaf.text().trim_start_matches('@'); - for (label, detail) in analyze_labels(world, frames).0 { - if label.0 == target { - return Some(Tooltip::Text(detail?)); - } - } - - None -} - -/// Tooltips for components of a named parameter. -fn named_param_tooltip( - world: &(dyn World + 'static), - leaf: &LinkedNode, -) -> Option { - let (info, named) = if_chain! { - // Ensure that we are in a named pair in the arguments to a function - // call or set rule. - if let Some(parent) = leaf.parent(); - if let Some(named) = parent.cast::(); - if let Some(grand) = parent.parent(); - if matches!(grand.kind(), SyntaxKind::Args); - if let Some(grand_grand) = grand.parent(); - if let Some(expr) = grand_grand.cast::(); - if let Some(ast::Expr::Ident(callee)) = match expr { - ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), - _ => None, - }; - - // Find metadata about the function. - if let Some(Value::Func(func)) = world.library().global.scope().get(&callee); - if let Some(info) = func.info(); - then { (info, named) } - else { return None; } - }; - - // Hovering over the parameter name. - if_chain! { - if leaf.index() == 0; - if let Some(ident) = leaf.cast::(); - if let Some(param) = info.param(&ident); - then { - return Some(Tooltip::Text(plain_docs_sentence(param.docs))); - } - } - - // Hovering over a string parameter value. - if_chain! { - if let Some(string) = leaf.cast::(); - if let Some(param) = info.param(&named.name()); - if let Some(docs) = find_string_doc(¶m.cast, &string.get()); - then { - return Some(Tooltip::Text(docs.into())); - } - } - - None -} - -/// Find documentation for a castable string. -fn find_string_doc(info: &CastInfo, string: &str) -> Option<&'static str> { - match info { - CastInfo::Value(Value::Str(s), docs) if s.as_str() == string => Some(docs), - CastInfo::Union(options) => { - options.iter().find_map(|option| find_string_doc(option, string)) - } - _ => None, - } -} - -/// Tooltip for font. -fn font_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { - if_chain! { - // Ensure that we are on top of a string. - if let Some(string) = leaf.cast::(); - let lower = string.get().to_lowercase(); - - // Ensure that we are in the arguments to the text function. - if let Some(parent) = leaf.parent(); - if let Some(named) = parent.cast::(); - if named.name().as_str() == "font"; - - // Find the font family. - if let Some((_, iter)) = world - .book() - .families() - .find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str()); - - then { - let detail = summarize_font_family(iter); - return Some(Tooltip::Text(detail)); - } - }; - - None -} diff --git a/src/image.rs b/src/image.rs deleted file mode 100644 index 3a245c14..00000000 --- a/src/image.rs +++ /dev/null @@ -1,449 +0,0 @@ -//! Image handling. - -use std::cell::RefCell; -use std::collections::BTreeMap; -use std::fmt::{self, Debug, Formatter}; -use std::io; -use std::sync::Arc; - -use comemo::{Prehashed, Track, Tracked}; -use ecow::{EcoString, EcoVec}; -use image::codecs::gif::GifDecoder; -use image::codecs::jpeg::JpegDecoder; -use image::codecs::png::PngDecoder; -use image::io::Limits; -use image::{ImageDecoder, ImageResult}; -use usvg::{TreeParsing, TreeTextToPath}; - -use crate::diag::{format_xml_like_error, StrResult}; -use crate::font::Font; -use crate::geom::Axes; -use crate::util::Bytes; -use crate::World; - -/// A raster or vector image. -/// -/// Values of this type are cheap to clone and hash. -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Image(Arc>); - -/// The internal representation. -#[derive(Hash)] -struct Repr { - /// The raw, undecoded image data. - data: Bytes, - /// The format of the encoded `buffer`. - format: ImageFormat, - /// The size of the image. - size: Axes, - /// A loader for fonts referenced by an image (currently, only applies to - /// SVG). - loader: PreparedLoader, - /// A text describing the image. - alt: Option, -} - -impl Image { - /// Create an image from a buffer and a format. - #[comemo::memoize] - pub fn new( - data: Bytes, - format: ImageFormat, - alt: Option, - ) -> StrResult { - let loader = PreparedLoader::default(); - let decoded = match format { - ImageFormat::Raster(format) => decode_raster(&data, format)?, - ImageFormat::Vector(VectorFormat::Svg) => { - decode_svg(&data, (&loader as &dyn SvgFontLoader).track())? - } - }; - - Ok(Self(Arc::new(Prehashed::new(Repr { - data, - format, - size: decoded.size(), - loader, - alt, - })))) - } - - /// Create a font-dependant image from a buffer and a format. - #[comemo::memoize] - pub fn with_fonts( - data: Bytes, - format: ImageFormat, - world: Tracked, - fallback_family: Option<&str>, - alt: Option, - ) -> StrResult { - let loader = WorldLoader::new(world, fallback_family); - let decoded = match format { - ImageFormat::Raster(format) => decode_raster(&data, format)?, - ImageFormat::Vector(VectorFormat::Svg) => { - decode_svg(&data, (&loader as &dyn SvgFontLoader).track())? - } - }; - - Ok(Self(Arc::new(Prehashed::new(Repr { - data, - format, - size: decoded.size(), - loader: loader.into_prepared(), - alt, - })))) - } - - /// The raw image data. - pub fn data(&self) -> &Bytes { - &self.0.data - } - - /// The format of the image. - pub fn format(&self) -> ImageFormat { - self.0.format - } - - /// The size of the image in pixels. - pub fn size(&self) -> Axes { - self.0.size - } - - /// The width of the image in pixels. - pub fn width(&self) -> u32 { - self.size().x - } - - /// The height of the image in pixels. - pub fn height(&self) -> u32 { - self.size().y - } - - /// A text describing the image. - pub fn alt(&self) -> Option<&str> { - self.0.alt.as_deref() - } - - /// The decoded version of the image. - pub fn decoded(&self) -> Arc { - match self.format() { - ImageFormat::Raster(format) => decode_raster(self.data(), format), - ImageFormat::Vector(VectorFormat::Svg) => { - decode_svg(self.data(), (&self.0.loader as &dyn SvgFontLoader).track()) - } - } - .unwrap() - } -} - -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Image") - .field("format", &self.format()) - .field("width", &self.width()) - .field("height", &self.height()) - .field("alt", &self.alt()) - .finish() - } -} - -/// A raster or vector image format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ImageFormat { - /// A raster graphics format. - Raster(RasterFormat), - /// A vector graphics format. - Vector(VectorFormat), -} - -/// A raster graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum RasterFormat { - /// Raster format for illustrations and transparent graphics. - Png, - /// Lossy raster format suitable for photos. - Jpg, - /// Raster format that is typically used for short animated clips. - Gif, -} - -/// A vector graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum VectorFormat { - /// The vector graphics format of the web. - Svg, -} - -impl From for image::ImageFormat { - fn from(format: RasterFormat) -> Self { - match format { - RasterFormat::Png => image::ImageFormat::Png, - RasterFormat::Jpg => image::ImageFormat::Jpeg, - RasterFormat::Gif => image::ImageFormat::Gif, - } - } -} - -impl From for RasterFormat { - fn from(format: ttf_parser::RasterImageFormat) -> Self { - match format { - ttf_parser::RasterImageFormat::PNG => RasterFormat::Png, - } - } -} - -impl From for ImageFormat { - fn from(format: ttf_parser::RasterImageFormat) -> Self { - Self::Raster(format.into()) - } -} - -/// A decoded image. -pub enum DecodedImage { - /// A decoded pixel raster with its ICC profile. - Raster(image::DynamicImage, Option, RasterFormat), - /// An decoded SVG tree. - Svg(usvg::Tree), -} - -impl DecodedImage { - /// The size of the image in pixels. - pub fn size(&self) -> Axes { - Axes::new(self.width(), self.height()) - } - - /// The width of the image in pixels. - pub fn width(&self) -> u32 { - match self { - Self::Raster(dynamic, _, _) => dynamic.width(), - Self::Svg(tree) => tree.size.width().ceil() as u32, - } - } - - /// The height of the image in pixels. - pub fn height(&self) -> u32 { - match self { - Self::Raster(dynamic, _, _) => dynamic.height(), - Self::Svg(tree) => tree.size.height().ceil() as u32, - } - } -} - -/// Raw data for of an ICC profile. -pub struct IccProfile(pub Vec); - -/// Decode a raster image. -#[comemo::memoize] -fn decode_raster(data: &Bytes, format: RasterFormat) -> StrResult> { - fn decode_with<'a, T: ImageDecoder<'a>>( - decoder: ImageResult, - ) -> ImageResult<(image::DynamicImage, Option)> { - let mut decoder = decoder?; - let icc = decoder.icc_profile().filter(|data| !data.is_empty()).map(IccProfile); - decoder.set_limits(Limits::default())?; - let dynamic = image::DynamicImage::from_decoder(decoder)?; - Ok((dynamic, icc)) - } - - let cursor = io::Cursor::new(data); - let (dynamic, icc) = match format { - RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)), - RasterFormat::Png => decode_with(PngDecoder::new(cursor)), - RasterFormat::Gif => decode_with(GifDecoder::new(cursor)), - } - .map_err(format_image_error)?; - - Ok(Arc::new(DecodedImage::Raster(dynamic, icc, format))) -} - -/// Decode an SVG image. -#[comemo::memoize] -fn decode_svg( - data: &Bytes, - loader: Tracked, -) -> StrResult> { - // Disable usvg's default to "Times New Roman". Instead, we default to - // the empty family and later, when we traverse the SVG, we check for - // empty and non-existing family names and replace them with the true - // fallback family. This way, we can memoize SVG decoding with and without - // fonts if the SVG does not contain text. - let opts = usvg::Options { font_family: String::new(), ..Default::default() }; - let mut tree = usvg::Tree::from_data(data, &opts).map_err(format_usvg_error)?; - if tree.has_text_nodes() { - let fontdb = load_svg_fonts(&tree, loader); - tree.convert_text(&fontdb); - } - Ok(Arc::new(DecodedImage::Svg(tree))) -} - -/// Discover and load the fonts referenced by an SVG. -fn load_svg_fonts( - tree: &usvg::Tree, - loader: Tracked, -) -> fontdb::Database { - let mut referenced = BTreeMap::::new(); - let mut fontdb = fontdb::Database::new(); - let mut load = |family_cased: &str| { - let family = EcoString::from(family_cased.trim()).to_lowercase(); - if let Some(&success) = referenced.get(&family) { - return success; - } - - // We load all variants for the family, since we don't know which will - // be used. - let mut success = false; - for font in loader.load(&family) { - let source = Arc::new(font.data().clone()); - fontdb.load_font_source(fontdb::Source::Binary(source)); - success = true; - } - - referenced.insert(family, success); - success - }; - - // Load fallback family. - let fallback_cased = loader.fallback(); - if let Some(family_cased) = fallback_cased { - load(family_cased); - } - - // Find out which font families are referenced by the SVG. - traverse_svg(&tree.root, &mut |node| { - let usvg::NodeKind::Text(text) = &mut *node.borrow_mut() else { return }; - for chunk in &mut text.chunks { - for span in &mut chunk.spans { - for family_cased in &mut span.font.families { - if family_cased.is_empty() || !load(family_cased) { - let Some(fallback) = fallback_cased else { continue }; - *family_cased = fallback.into(); - } - } - } - } - }); - - fontdb -} - -/// Search for all font families referenced by an SVG. -fn traverse_svg(node: &usvg::Node, f: &mut F) -where - F: FnMut(&usvg::Node), -{ - f(node); - for child in node.children() { - traverse_svg(&child, f); - } -} - -/// Interface for loading fonts for an SVG. -/// -/// Can be backed by a `WorldLoader` or a `PreparedLoader`. The first is used -/// when the image is initially decoded. It records all required fonts and -/// produces a `PreparedLoader` from it. This loader can then be used to -/// redecode the image with a cache hit from the initial decoding. This way, we -/// can cheaply access the decoded version of an image. -/// -/// The alternative would be to store the decoded image directly in the image, -/// but that would make `Image` not `Send` because `usvg::Tree` is not `Send`. -/// The current design also has the added benefit that large decoded images can -/// be evicted if they are not used anymore. -#[comemo::track] -trait SvgFontLoader { - /// Load all fonts for the given lowercased font family. - fn load(&self, lower_family: &str) -> EcoVec; - - /// The case-sensitive name of the fallback family. - fn fallback(&self) -> Option<&str>; -} - -/// Loads fonts for an SVG from a world -struct WorldLoader<'a> { - world: Tracked<'a, dyn World + 'a>, - seen: RefCell>>, - fallback_family_cased: Option, -} - -impl<'a> WorldLoader<'a> { - fn new(world: Tracked<'a, dyn World + 'a>, fallback_family: Option<&str>) -> Self { - // Recover the non-lowercased version of the family because - // usvg is case sensitive. - let book = world.book(); - let fallback_family_cased = fallback_family - .and_then(|lowercase| book.select_family(lowercase).next()) - .and_then(|index| book.info(index)) - .map(|info| info.family.clone()); - - Self { - world, - fallback_family_cased, - seen: Default::default(), - } - } - - fn into_prepared(self) -> PreparedLoader { - PreparedLoader { - families: self.seen.into_inner(), - fallback_family_cased: self.fallback_family_cased, - } - } -} - -impl SvgFontLoader for WorldLoader<'_> { - fn load(&self, family: &str) -> EcoVec { - self.seen - .borrow_mut() - .entry(family.into()) - .or_insert_with(|| { - self.world - .book() - .select_family(family) - .filter_map(|id| self.world.font(id)) - .collect() - }) - .clone() - } - - fn fallback(&self) -> Option<&str> { - self.fallback_family_cased.as_deref() - } -} - -/// Loads fonts for an SVG from a prepared list. -#[derive(Default, Hash)] -struct PreparedLoader { - families: BTreeMap>, - fallback_family_cased: Option, -} - -impl SvgFontLoader for PreparedLoader { - fn load(&self, family: &str) -> EcoVec { - self.families.get(family).cloned().unwrap_or_default() - } - - fn fallback(&self) -> Option<&str> { - self.fallback_family_cased.as_deref() - } -} - -/// Format the user-facing raster graphic decoding error message. -fn format_image_error(error: image::ImageError) -> EcoString { - match error { - image::ImageError::Limits(_) => "file is too large".into(), - _ => "failed to decode image".into(), - } -} - -/// Format the user-facing SVG decoding error message. -fn format_usvg_error(error: usvg::Error) -> EcoString { - match error { - usvg::Error::NotAnUtf8Str => "file is not valid utf-8".into(), - usvg::Error::MalformedGZip => "file is not compressed correctly".into(), - usvg::Error::ElementsLimitReached => "file is too large".into(), - usvg::Error::InvalidSize => { - "failed to parse svg: width, height, or viewbox is invalid".into() - } - usvg::Error::ParsingFailed(error) => format_xml_like_error("svg", error), - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 8b3d1d3d..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! The compiler for the _Typst_ markup language. -//! -//! # Steps -//! - **Parsing:** -//! The compiler first transforms a plain string into an iterator of [tokens]. -//! This token stream is [parsed] into a [syntax tree]. The tree itself is -//! untyped, but the [AST] module provides a typed layer over it. -//! - **Evaluation:** -//! The next step is to [evaluate] the markup. This produces a [module], -//! consisting of a scope of values that were exported by the code and -//! [content], a hierarchical, styled representation of what was written in -//! the source file. The elements of the content tree are well structured and -//! order-independent and thus much better suited for further processing than -//! the raw markup. -//! - **Typesetting:** -//! Next, the content is [typeset] into a [document] containing one [frame] -//! per page with items at fixed positions. -//! - **Exporting:** -//! These frames can finally be exported into an output format (currently -//! supported are [PDF] and [raster images]). -//! -//! [tokens]: syntax::SyntaxKind -//! [parsed]: syntax::parse -//! [syntax tree]: syntax::SyntaxNode -//! [AST]: syntax::ast -//! [evaluate]: eval::eval -//! [module]: eval::Module -//! [content]: model::Content -//! [typeset]: model::typeset -//! [document]: doc::Document -//! [frame]: doc::Frame -//! [PDF]: export::pdf -//! [raster images]: export::render - -#![recursion_limit = "1000"] -#![allow(clippy::comparison_chain)] - -extern crate self as typst; - -#[macro_use] -pub mod util; -#[macro_use] -pub mod diag; -#[macro_use] -pub mod eval; -pub mod doc; -pub mod export; -pub mod file; -pub mod font; -pub mod geom; -pub mod ide; -pub mod image; -pub mod model; -pub mod syntax; - -use comemo::{Prehashed, Track, TrackedMut}; -use ecow::EcoString; - -use crate::diag::{FileResult, SourceResult}; -use crate::doc::Document; -use crate::eval::{Datetime, Library, Route, Tracer}; -use crate::file::{FileId, PackageSpec}; -use crate::font::{Font, FontBook}; -use crate::syntax::Source; -use crate::util::Bytes; - -/// Compile a source file into a fully layouted document. -#[tracing::instrument(skip(world))] -pub fn compile(world: &dyn World) -> SourceResult { - let route = Route::default(); - let mut tracer = Tracer::default(); - - // Call `track` just once to keep comemo's ID stable. - let world = world.track(); - let mut tracer = tracer.track_mut(); - - // Evaluate the source file into a module. - tracing::info!("Starting evaluation"); - let module = eval::eval( - world, - route.track(), - TrackedMut::reborrow_mut(&mut tracer), - &world.main(), - )?; - - // Typeset the module's contents. - model::typeset(world, tracer, &module.content()) -} - -/// The environment in which typesetting occurs. -/// -/// All loading functions (`main`, `source`, `file`, `font`) should perform -/// internal caching so that they are relatively cheap on repeated invocations -/// with the same argument. [`Source`], [`Bytes`], and [`Font`] are -/// all reference-counted and thus cheap to clone. -/// -/// The compiler doesn't do the caching itself because the world has much more -/// information on when something can change. For example, fonts typically don't -/// change and can thus even be cached across multiple compilations (for -/// long-running applications like `typst watch`). Source files on the other -/// hand can change and should thus be cleared after. Advanced clients like -/// language servers can also retain the source files and [edited](Source::edit) -/// them in-place to benefit from better incremental performance. -#[comemo::track] -pub trait World { - /// The standard library. - fn library(&self) -> &Prehashed; - - /// Metadata about all known fonts. - fn book(&self) -> &Prehashed; - - /// Access the main source file. - fn main(&self) -> Source; - - /// Try to access the specified source file. - /// - /// The returned `Source` file's [id](Source::id) does not have to match the - /// given `id`. Due to symlinks, two different file id's can point to the - /// same on-disk file. Implementors can deduplicate and return the same - /// `Source` if they want to, but do not have to. - fn source(&self, id: FileId) -> FileResult; - - /// Try to access the specified file. - fn file(&self, id: FileId) -> FileResult; - - /// Try to access the font with the given index in the font book. - fn font(&self, index: usize) -> Option; - - /// Get the current date. - /// - /// If no offset is specified, the local date should be chosen. Otherwise, - /// the UTC date should be chosen with the corresponding offset in hours. - /// - /// If this function returns `None`, Typst's `datetime` function will - /// return an error. - fn today(&self, offset: Option) -> Option; - - /// A list of all available packages and optionally descriptions for them. - /// - /// This function is optional to implement. It enhances the user experience - /// by enabling autocompletion for packages. Details about packages from the - /// `@preview` namespace are available from - /// `https://packages.typst.org/preview/index.json`. - fn packages(&self) -> &[(PackageSpec, Option)] { - &[] - } -} diff --git a/src/model/content.rs b/src/model/content.rs deleted file mode 100644 index 015f8b76..00000000 --- a/src/model/content.rs +++ /dev/null @@ -1,614 +0,0 @@ -use std::any::TypeId; -use std::fmt::{self, Debug, Formatter, Write}; -use std::iter::Sum; -use std::ops::{Add, AddAssign}; - -use comemo::Prehashed; -use ecow::{eco_format, EcoString, EcoVec}; - -use super::{ - element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable, - Location, Recipe, Selector, Style, Styles, Synthesize, -}; -use crate::diag::{SourceResult, StrResult}; -use crate::doc::Meta; -use crate::eval::{Dict, FromValue, IntoValue, Str, Value, Vm}; -use crate::syntax::Span; -use crate::util::pretty_array_like; - -/// Composable representation of styled content. -#[derive(Clone, Hash)] -#[allow(clippy::derived_hash_with_manual_eq)] -pub struct Content { - func: ElemFunc, - attrs: EcoVec, -} - -/// Attributes that can be attached to content. -#[derive(Debug, Clone, PartialEq, Hash)] -enum Attr { - Span(Span), - Field(EcoString), - Value(Prehashed), - Child(Prehashed), - Styles(Styles), - Prepared, - Guard(Guard), - Location(Location), -} - -impl Content { - /// Create an empty element. - pub fn new(func: ElemFunc) -> Self { - Self { func, attrs: EcoVec::new() } - } - - /// Create empty content. - pub fn empty() -> Self { - Self::new(SequenceElem::func()) - } - - /// Create a new sequence element from multiples elements. - pub fn sequence(iter: impl IntoIterator) -> Self { - let mut iter = iter.into_iter(); - let Some(first) = iter.next() else { return Self::empty() }; - let Some(second) = iter.next() else { return first }; - let mut content = Content::empty(); - content.attrs.push(Attr::Child(Prehashed::new(first))); - content.attrs.push(Attr::Child(Prehashed::new(second))); - content - .attrs - .extend(iter.map(|child| Attr::Child(Prehashed::new(child)))); - content - } - - /// The element function of the contained content. - pub fn func(&self) -> ElemFunc { - self.func - } - - /// Whether the content is an empty sequence. - pub fn is_empty(&self) -> bool { - self.is::() && self.attrs.is_empty() - } - - /// Whether the contained element is of type `T`. - pub fn is(&self) -> bool { - self.func == T::func() - } - - /// Cast to `T` if the contained element is of type `T`. - pub fn to(&self) -> Option<&T> { - T::unpack(self) - } - - /// Access the children if this is a sequence. - pub fn to_sequence(&self) -> Option> { - if !self.is::() { - return None; - } - Some(self.attrs.iter().filter_map(Attr::child)) - } - - /// Access the child and styles. - pub fn to_styled(&self) -> Option<(&Content, &Styles)> { - if !self.is::() { - return None; - } - let child = self.attrs.iter().find_map(Attr::child)?; - let styles = self.attrs.iter().find_map(Attr::styles)?; - Some((child, styles)) - } - - /// Whether the contained element has the given capability. - pub fn can(&self) -> bool - where - C: ?Sized + 'static, - { - (self.func.0.vtable)(TypeId::of::()).is_some() - } - - /// Whether the contained element has the given capability. - /// Where the capability is given by a `TypeId`. - pub fn can_type_id(&self, type_id: TypeId) -> bool { - (self.func.0.vtable)(type_id).is_some() - } - - /// Cast to a trait object if the contained element has the given - /// capability. - pub fn with(&self) -> Option<&C> - where - C: ?Sized + 'static, - { - let vtable = (self.func.0.vtable)(TypeId::of::())?; - let data = self as *const Self as *const (); - Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) }) - } - - /// Cast to a mutable trait object if the contained element has the given - /// capability. - pub fn with_mut(&mut self) -> Option<&mut C> - where - C: ?Sized + 'static, - { - let vtable = (self.func.0.vtable)(TypeId::of::())?; - let data = self as *mut Self as *mut (); - Some(unsafe { &mut *crate::util::fat::from_raw_parts_mut(data, vtable) }) - } - - /// The content's span. - pub fn span(&self) -> Span { - self.attrs.iter().find_map(Attr::span).unwrap_or(Span::detached()) - } - - /// Attach a span to the content if it doesn't already have one. - pub fn spanned(mut self, span: Span) -> Self { - if self.span().is_detached() { - self.attrs.push(Attr::Span(span)); - } - self - } - - /// Attach a field to the content. - pub fn with_field( - mut self, - name: impl Into, - value: impl IntoValue, - ) -> Self { - self.push_field(name, value); - self - } - - /// Attach a field to the content. - pub fn push_field(&mut self, name: impl Into, value: impl IntoValue) { - let name = name.into(); - if let Some(i) = self.attrs.iter().position(|attr| match attr { - Attr::Field(field) => *field == name, - _ => false, - }) { - self.attrs.make_mut()[i + 1] = - Attr::Value(Prehashed::new(value.into_value())); - } else { - self.attrs.push(Attr::Field(name)); - self.attrs.push(Attr::Value(Prehashed::new(value.into_value()))); - } - } - - /// Access a field on the content. - pub fn field(&self, name: &str) -> Option { - if let (Some(iter), "children") = (self.to_sequence(), name) { - Some(Value::Array(iter.cloned().map(Value::Content).collect())) - } else if let (Some((child, _)), "child") = (self.to_styled(), name) { - Some(Value::Content(child.clone())) - } else { - self.field_ref(name).cloned() - } - } - - /// Access a field on the content by reference. - /// - /// Does not include synthesized fields for sequence and styled elements. - pub fn field_ref(&self, name: &str) -> Option<&Value> { - self.fields_ref() - .find(|&(field, _)| field == name) - .map(|(_, value)| value) - } - - /// Iter over all fields on the content. - /// - /// Does not include synthesized fields for sequence and styled elements. - pub fn fields(&self) -> impl Iterator { - static CHILD: EcoString = EcoString::inline("child"); - static CHILDREN: EcoString = EcoString::inline("children"); - - let option = if let Some(iter) = self.to_sequence() { - Some((&CHILDREN, Value::Array(iter.cloned().map(Value::Content).collect()))) - } else if let Some((child, _)) = self.to_styled() { - Some((&CHILD, Value::Content(child.clone()))) - } else { - None - }; - - self.fields_ref() - .map(|(name, value)| (name, value.clone())) - .chain(option) - } - - /// Iter over all fields on the content. - /// - /// Does not include synthesized fields for sequence and styled elements. - pub fn fields_ref(&self) -> impl Iterator { - let mut iter = self.attrs.iter(); - std::iter::from_fn(move || { - let field = iter.find_map(Attr::field)?; - let value = iter.next()?.value()?; - Some((field, value)) - }) - } - - /// Try to access a field on the content as a specified type. - pub fn cast_field(&self, name: &str) -> Option { - match self.field(name) { - Some(value) => value.cast().ok(), - None => None, - } - } - - /// Expect a field on the content to exist as a specified type. - #[track_caller] - pub fn expect_field(&self, name: &str) -> T { - self.field(name).unwrap().cast().unwrap() - } - - /// Whether the content has the specified field. - pub fn has(&self, field: &str) -> bool { - self.field(field).is_some() - } - - /// Borrow the value of the given field. - pub fn at(&self, field: &str, default: Option) -> StrResult { - self.field(field) - .or(default) - .ok_or_else(|| missing_field_no_default(field)) - } - - /// Return the fields of the content as a dict. - pub fn dict(&self) -> Dict { - self.fields() - .map(|(key, value)| (key.to_owned().into(), value)) - .collect() - } - - /// The content's label. - pub fn label(&self) -> Option<&Label> { - match self.field_ref("label")? { - Value::Label(label) => Some(label), - _ => None, - } - } - - /// Attach a label to the content. - pub fn labelled(self, label: Label) -> Self { - self.with_field("label", label) - } - - /// Style this content with a style entry. - pub fn styled(mut self, style: impl Into