summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /src
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'src')
-rw-r--r--src/diag.rs376
-rw-r--r--src/doc.rs719
-rw-r--r--src/eval/args.rs216
-rw-r--r--src/eval/array.rs508
-rw-r--r--src/eval/auto.rs39
-rw-r--r--src/eval/cast.rs316
-rw-r--r--src/eval/datetime.rs201
-rw-r--r--src/eval/dict.rs235
-rw-r--r--src/eval/func.rs643
-rw-r--r--src/eval/int.rs81
-rw-r--r--src/eval/library.rs182
-rw-r--r--src/eval/methods.rs373
-rw-r--r--src/eval/mod.rs1908
-rw-r--r--src/eval/module.rs98
-rw-r--r--src/eval/none.rs74
-rw-r--r--src/eval/ops.rs429
-rw-r--r--src/eval/scope.rs178
-rw-r--r--src/eval/str.rs620
-rw-r--r--src/eval/symbol.rs210
-rw-r--r--src/eval/value.rs461
-rw-r--r--src/export/mod.rs7
-rw-r--r--src/export/pdf/font.rs204
-rw-r--r--src/export/pdf/image.rs143
-rw-r--r--src/export/pdf/mod.rs235
-rw-r--r--src/export/pdf/outline.rs127
-rw-r--r--src/export/pdf/page.rs565
-rw-r--r--src/export/render.rs673
-rw-r--r--src/file.rs303
-rw-r--r--src/font/book.rs546
-rw-r--r--src/font/mod.rs249
-rw-r--r--src/font/variant.rs270
-rw-r--r--src/geom/abs.rs266
-rw-r--r--src/geom/align.rs239
-rw-r--r--src/geom/angle.rs188
-rw-r--r--src/geom/axes.rs305
-rw-r--r--src/geom/color.rs386
-rw-r--r--src/geom/corners.rs219
-rw-r--r--src/geom/dir.rs79
-rw-r--r--src/geom/ellipse.rs22
-rw-r--r--src/geom/em.rs153
-rw-r--r--src/geom/fr.rs119
-rw-r--r--src/geom/length.rs128
-rw-r--r--src/geom/macros.rs47
-rw-r--r--src/geom/mod.rs121
-rw-r--r--src/geom/paint.rs30
-rw-r--r--src/geom/path.rs54
-rw-r--r--src/geom/point.rs146
-rw-r--r--src/geom/ratio.rs133
-rw-r--r--src/geom/rel.rs246
-rw-r--r--src/geom/rounded.rs182
-rw-r--r--src/geom/scalar.rs175
-rw-r--r--src/geom/shape.rs35
-rw-r--r--src/geom/sides.rs268
-rw-r--r--src/geom/size.rs78
-rw-r--r--src/geom/smart.rs146
-rw-r--r--src/geom/stroke.rs387
-rw-r--r--src/geom/transform.rs77
-rw-r--r--src/ide/analyze.rs111
-rw-r--r--src/ide/complete.rs1201
-rw-r--r--src/ide/highlight.rs430
-rw-r--r--src/ide/jump.rs173
-rw-r--r--src/ide/mod.rs97
-rw-r--r--src/ide/tooltip.rs222
-rw-r--r--src/image.rs449
-rw-r--r--src/lib.rs147
-rw-r--r--src/model/content.rs614
-rw-r--r--src/model/element.rs134
-rw-r--r--src/model/introspect.rs352
-rw-r--r--src/model/label.rs16
-rw-r--r--src/model/mod.rs148
-rw-r--r--src/model/realize.rs228
-rw-r--r--src/model/selector.rs296
-rw-r--r--src/model/styles.rs750
-rw-r--r--src/syntax/ast.rs1994
-rw-r--r--src/syntax/kind.rs448
-rw-r--r--src/syntax/lexer.rs738
-rw-r--r--src/syntax/mod.rs23
-rw-r--r--src/syntax/node.rs889
-rw-r--r--src/syntax/parser.rs1643
-rw-r--r--src/syntax/reparser.rs322
-rw-r--r--src/syntax/source.rs421
-rw-r--r--src/syntax/span.rs148
-rw-r--r--src/util/bytes.rs59
-rw-r--r--src/util/fat.rs55
-rw-r--r--src/util/mod.rs268
85 files changed, 0 insertions, 27794 deletions
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<T> = Result<T, Box<Vec<SourceError>>>;
-
-/// 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<Spanned<Tracepoint>>,
- /// Additonal hints to the user, indicating how this error could be avoided
- /// or worked around.
- pub hints: Vec<EcoString>,
-}
-
-impl SourceError {
- /// Create a new, bare error.
- pub fn new(span: Span, message: impl Into<EcoString>) -> 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<Item = EcoString>) -> 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<EcoString>),
- /// 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<T> {
- /// Add the tracepoint to all errors that lie outside the `span`.
- fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
- where
- F: Fn() -> Tracepoint;
-}
-
-impl<T> Trace<T> for SourceResult<T> {
- fn trace<F>(self, world: Tracked<dyn World + '_>, 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<T> = Result<T, EcoString>;
-
-/// Convert a [`StrResult`] to a [`SourceResult`] by adding span information.
-pub trait At<T> {
- /// Add the span information.
- fn at(self, span: Span) -> SourceResult<T>;
-}
-
-impl<T, S> At<T> for Result<T, S>
-where
- S: Into<EcoString>,
-{
- fn at(self, span: Span) -> SourceResult<T> {
- self.map_err(|message| Box::new(vec![SourceError::new(span, message)]))
- }
-}
-
-/// A result type with a string error message and hints.
-pub type HintedStrResult<T> = Result<T, HintedString>;
-
-/// 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<EcoString>,
-}
-
-impl<T> At<T> for Result<T, HintedString> {
- fn at(self, span: Span) -> SourceResult<T> {
- 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<T> {
- /// Add the hint.
- fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>;
-}
-
-impl<T> Hint<T> for StrResult<T> {
- fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
- self.map_err(|message| HintedString { message, hints: vec![hint.into()] })
- }
-}
-
-impl<T> Hint<T> for HintedStrResult<T> {
- fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
- self.map_err(|mut error| {
- error.hints.push(hint.into());
- error
- })
- }
-}
-
-/// A result type with a file-related error.
-pub type FileResult<T> = Result<T, FileError>;
-
-/// 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<Utf8Error> for FileError {
- fn from(_: Utf8Error) -> Self {
- Self::InvalidUtf8
- }
-}
-
-impl From<FromUtf8Error> for FileError {
- fn from(_: FromUtf8Error) -> Self {
- Self::InvalidUtf8
- }
-}
-
-impl From<PackageError> for FileError {
- fn from(error: PackageError) -> Self {
- Self::Package(error)
- }
-}
-
-impl From<FileError> for EcoString {
- fn from(error: FileError) -> Self {
- eco_format!("{error}")
- }
-}
-
-/// A result type with a package-related error.
-pub type PackageResult<T> = Result<T, PackageError>;
-
-/// 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<PackageError> 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<Frame>,
- /// The document's title.
- pub title: Option<EcoString>,
- /// The document's author.
- pub author: Vec<EcoString>,
-}
-
-/// 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<Abs>,
- /// The items composing this layout.
- items: Arc<Vec<(Point, FrameItem)>>,
-}
-
-/// 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<I>(&mut self, items: I)
- where
- I: IntoIterator<Item = (Point, FrameItem)>,
- {
- 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<Align>) {
- 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<Item = Meta>) {
- 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<Paint>,
- stroke: Sides<Option<Stroke>>,
- outset: Sides<Rel<Abs>>,
- radius: Corners<Rel<Abs>>,
- 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<F>(&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<Glyph>,
-}
-
-impl TextItem {
- /// The width of the text run.
- pub fn width(&self) -> Abs {
- self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().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<u16>,
- /// 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> {
- 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<Self, Self::Err> {
- 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<Self, Self::Err> {
- 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<Position> 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<T: Send>() {}
- ensure_send::<Document>();
- }
-}
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<Arg>,
-}
-
-/// 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<Str>,
- /// The value of the argument.
- pub value: Spanned<Value>,
-}
-
-impl Args {
- /// Create positional arguments from a span and values.
- pub fn new<T: IntoValue>(span: Span, values: impl IntoIterator<Item = T>) -> 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<T>(&mut self) -> SourceResult<Option<T>>
- where
- T: FromValue<Spanned<Value>>,
- {
- 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<Vec<Arg>> {
- 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<T>(&mut self, what: &str) -> SourceResult<T>
- where
- T: FromValue<Spanned<Value>>,
- {
- 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<T>(&mut self) -> SourceResult<Option<T>>
- where
- T: FromValue<Spanned<Value>>,
- {
- 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<T>(&mut self) -> SourceResult<Vec<T>>
- where
- T: FromValue<Spanned<Value>>,
- {
- 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<T>(&mut self, name: &str) -> SourceResult<Option<T>>
- where
- T: FromValue<Spanned<Value>>,
- {
- // 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<T>(&mut self, name: &str) -> SourceResult<Option<T>>
- where
- T: FromValue<Spanned<Value>>,
- {
- 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<Value>);
-
-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<Value> {
- 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<Value> {
- 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<i64>) -> StrResult<Self> {
- 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<Option<Value>> {
- for item in self.iter() {
- let args = Args::new(func.span(), [item.clone()]);
- if func.call_vm(vm, args)?.cast::<bool>().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<Option<i64>> {
- for (i, item) in self.iter().enumerate() {
- let args = Args::new(func.span(), [item.clone()]);
- if func.call_vm(vm, args)?.cast::<bool>().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<Self> {
- 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::<bool>().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> {
- 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<Value> {
- 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<Value>, span: Span) -> SourceResult<Value> {
- 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<Value>, span: Span) -> SourceResult<Value> {
- 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<bool> {
- for item in self.iter() {
- let args = Args::new(func.span(), [item.clone()]);
- if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? {
- return Ok(true);
- }
- }
-
- Ok(false)
- }
-
- /// Whether all items match.
- pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> {
- for item in self.iter() {
- let args = Args::new(func.span(), [item.clone()]);
- if !func.call_vm(vm, args)?.cast::<bool>().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<Value>, mut last: Option<Value>) -> StrResult<Value> {
- 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<Func>,
- ) -> SourceResult<Self> {
- 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<Self> {
- 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<Value> {
- self.0.iter()
- }
-
- /// Resolve an index.
- fn locate(&self, index: i64) -> Option<usize> {
- 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<Value> for Array {
- fn extend<T: IntoIterator<Item = Value>>(&mut self, iter: T) {
- self.0.extend(iter);
- }
-}
-
-impl FromIterator<Value> for Array {
- fn from_iter<T: IntoIterator<Item = Value>>(iter: T) -> Self {
- Self(iter.into_iter().collect())
- }
-}
-
-impl IntoIterator for Array {
- type Item = Value;
- type IntoIter = ecow::vec::IntoIter<Value>;
-
- 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<EcoVec<Value>> for Array {
- fn from(v: EcoVec<Value>) -> Self {
- Array(v)
- }
-}
-
-impl From<&[Value]> for Array {
- fn from(v: &[Value]) -> Self {
- Array(v.into())
- }
-}
-
-impl<T> Reflect for Vec<T> {
- fn describe() -> CastInfo {
- Array::describe()
- }
-
- fn castable(value: &Value) -> bool {
- Array::castable(value)
- }
-}
-
-impl<T: IntoValue> IntoValue for Vec<T> {
- fn into_value(self) -> Value {
- Value::Array(self.into_iter().map(IntoValue::into_value).collect())
- }
-}
-
-impl<T: FromValue> FromValue for Vec<T> {
- fn from_value(value: Value) -> StrResult<Self> {
- value.cast::<Array>()?.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<Self> {
- 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<Value>` due to conflicting impls. We could use
-/// `From<T> 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!(
- /// <Int as Reflect>::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<T: Reflect> Reflect for Spanned<T> {
- fn describe() -> CastInfo {
- T::describe()
- }
-
- fn castable(value: &Value) -> bool {
- T::castable(value)
- }
-}
-
-impl<T: Reflect> Reflect for StrResult<T> {
- fn describe() -> CastInfo {
- T::describe()
- }
-
- fn castable(value: &Value) -> bool {
- T::castable(value)
- }
-}
-
-impl<T: Reflect> Reflect for SourceResult<T> {
- fn describe() -> CastInfo {
- T::describe()
- }
-
- fn castable(value: &Value) -> bool {
- T::castable(value)
- }
-}
-
-impl<T: Reflect> Reflect for &T {
- fn describe() -> CastInfo {
- T::describe()
- }
-
- fn castable(value: &Value) -> bool {
- T::castable(value)
- }
-}
-
-impl<T: Reflect> 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<T: IntoValue> IntoValue for Spanned<T> {
- fn into_value(self) -> Value {
- self.v.into_value()
- }
-}
-
-/// Cast a Rust type or result into a [`SourceResult<Value>`].
-///
-/// Converts `T`, [`StrResult<T>`], or [`SourceResult<T>`] into
-/// [`SourceResult<Value>`] by `Ok`-wrapping or adding span information.
-pub trait IntoResult {
- /// Cast this type into a value.
- fn into_result(self, span: Span) -> SourceResult<Value>;
-}
-
-impl<T: IntoValue> IntoResult for T {
- fn into_result(self, _: Span) -> SourceResult<Value> {
- Ok(self.into_value())
- }
-}
-
-impl<T: IntoValue> IntoResult for StrResult<T> {
- fn into_result(self, span: Span) -> SourceResult<Value> {
- self.map(IntoValue::into_value).at(span)
- }
-}
-
-impl<T: IntoValue> IntoResult for SourceResult<T> {
- fn into_result(self, _: Span) -> SourceResult<Value> {
- self.map(IntoValue::into_value)
- }
-}
-
-/// Try to cast a Typst [`Value`] into a Rust type.
-///
-/// See also: [`Reflect`].
-pub trait FromValue<V = Value>: Sized + Reflect {
- /// Try to cast the value into an instance of `Self`.
- fn from_value(value: V) -> StrResult<Self>;
-}
-
-impl FromValue for Value {
- fn from_value(value: Value) -> StrResult<Self> {
- Ok(value)
- }
-}
-
-impl<T: FromValue> FromValue<Spanned<Value>> for T {
- fn from_value(value: Spanned<Value>) -> StrResult<Self> {
- T::from_value(value.v)
- }
-}
-
-impl<T: FromValue> FromValue<Spanned<Value>> for Spanned<T> {
- fn from_value(value: Spanned<Value>) -> StrResult<Self> {
- 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<Self>),
-}
-
-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<EcoString>,
- 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<T> Variadics for Vec<T> {
- 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<Self> {
- 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<EcoString>) -> Result<EcoString, EcoString> {
- 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<i32> {
- 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<u8> {
- 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<u8> {
- 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<u8> {
- 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<u8> {
- 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<u8> {
- 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<u8> {
- 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<Self> {
- 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<Self> {
- 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<Self> {
- 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::<EcoVec<_>>();
-
- 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<IndexMap<Str, Value>>);
-
-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<Value> {
- 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<Value> {
- 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<Str, Value> {
- 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<H: Hasher>(&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<T: IntoIterator<Item = (Str, Value)>>(&mut self, iter: T) {
- Arc::make_mut(&mut self.0).extend(iter);
- }
-}
-
-impl FromIterator<(Str, Value)> for Dict {
- fn from_iter<T: IntoIterator<Item = (Str, Value)>>(iter: T) -> Self {
- Self(Arc::new(iter.into_iter().collect()))
- }
-}
-
-impl IntoIterator for Dict {
- type Item = (Str, Value);
- type IntoIter = indexmap::map::IntoIter<Str, Value>;
-
- 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<IndexMap<Str, Value>> for Dict {
- fn from(map: IndexMap<Str, Value>) -> 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<Prehashed<Closure>>),
- /// 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<Value> {
- let _span = tracing::info_span!(
- "call",
- name = self.name().unwrap_or("<anon>"),
- 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<T: IntoValue>(
- &self,
- vt: &mut Vt,
- args: impl IntoIterator<Item = T>,
- ) -> SourceResult<Value> {
- 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<ElemFunc> {
- 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<Repr> for Func {
- fn from(repr: Repr) -> Self {
- Self { repr, span: Span::detached() }
- }
-}
-
-impl From<ElemFunc> 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<Value>,
- /// Details about the function.
- pub info: Lazy<FuncInfo>,
-}
-
-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<H: Hasher>(&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<ParamInfo>,
- /// 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<fn() -> 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<Ident>,
- /// Captured values from outer scopes.
- pub captured: Scope,
- /// The list of parameters.
- pub params: Vec<Param>,
- /// 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<Ident>),
-}
-
-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<dyn World + '_>,
- route: Tracked<Route>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- depth: usize,
- mut args: Args,
- ) -> SourceResult<Value> {
- 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::<Value>(ident)?)
- }
- ast::Pattern::Normal(_) => unreachable!(),
- _ => {
- pattern.define(
- &mut vm,
- args.expect::<Value>("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::<Value>(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<Closure> 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<Document>,
- /// 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<EcoString>,
- /// 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<EcoString>, 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>) -> Content,
- /// The keys contained in the bibliography and short descriptions of them.
- #[allow(clippy::type_complexity)]
- pub bibliography_keys:
- fn(introspector: Tracked<Introspector>) -> Vec<(EcoString, Option<EcoString>)>,
- /// 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<usize>, 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<Content>,
- b: Option<Content>,
- // Fixed positions.
- tl: Option<Content>,
- bl: Option<Content>,
- tr: Option<Content>,
- br: Option<Content>,
- ) -> 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<Content>, 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<Value>,
-}
-
-impl Debug for LangItems {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad("LangItems { .. }")
- }
-}
-
-impl Hash for LangItems {
- fn hash<H: Hasher>(&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<LangItems> = 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<Value> {
- 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::<EcoString>("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::<EcoString>("field")?).into_value(),
- "at" => content
- .at(&args.expect::<EcoString>("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::<Str>("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::<Location>() {
- 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::<Selector>() {
- match method {
- "or" => selector.clone().or(args.all::<Selector>()?).into_value(),
- "and" => selector.clone().and(args.all::<Selector>()?).into_value(),
- "before" => {
- let location = args.expect::<Selector>("selector")?;
- let inclusive =
- args.named_or_find::<bool>("inclusive")?.unwrap_or(true);
- selector.clone().before(location, inclusive).into_value()
- }
- "after" => {
- let location = args.expect::<Selector>("selector")?;
- let inclusive =
- args.named_or_find::<bool>("inclusive")?.unwrap_or(true);
- selector.clone().after(location, inclusive).into_value()
- }
- _ => return missing(),
- }
- } else if let Some(&datetime) = dynamic.downcast::<Datetime>() {
- 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<Value> {
- 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::<Str>("key")?, args.expect("value")?),
- "remove" => {
- output = dict.remove(&args.expect::<EcoString>("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::<Str>("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<dyn World + '_>,
- route: Tracked<Route>,
- tracer: TrackedMut<Tracer>,
- source: &Source,
-) -> SourceResult<Module> {
- // 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::<ast::Markup>() {
- 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<dyn World + '_>,
- code: &str,
- span: Span,
-) -> SourceResult<Value> {
- 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::<ast::Code>().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<FlowEvent>,
- /// The stack of scopes.
- scopes: Scopes<'a>,
- /// The current call depth.
- depth: usize,
- /// A span that is currently traced.
- traced: Option<Span>,
-}
-
-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<Value>),
-}
-
-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<Tracked<'a, Self, <Route<'static> as Validate>::Constraint>>,
- id: Option<FileId>,
-}
-
-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<Span>,
- values: Vec<Value>,
-}
-
-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<Span>) -> Self {
- Self { span, values: vec![] }
- }
-
- /// Get the traced values.
- pub fn finish(self) -> Vec<Value> {
- self.values
- }
-}
-
-#[comemo::track]
-impl Tracer {
- /// The traced span if it is part of the given source file.
- fn span(&self, id: FileId) -> Option<Span> {
- 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<Self::Output>;
-}
-
-impl Eval for ast::Markup {
- type Output = Content;
-
- fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
- eval_markup(vm, &mut self.exprs())
- }
-}
-
-/// Evaluate a stream of markup.
-fn eval_markup(
- vm: &mut Vm,
- exprs: &mut impl Iterator<Item = ast::Expr>,
-) -> SourceResult<Content> {
- 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::<dyn Unlabellable>())
- {
- *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<Self::Output> {
- 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<Content> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- Ok(Content::sequence(
- self.exprs()
- .map(|expr| expr.eval_display(vm))
- .collect::<SourceResult<Vec<_>>>()?,
- ))
- }
-}
-
-impl Eval for ast::MathIdent {
- type Output = Value;
-
- #[tracing::instrument(name = "MathIdent::eval", skip_all)]
- fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- Ok(Value::None)
- }
-}
-
-impl Eval for ast::Auto {
- type Output = Value;
-
- #[tracing::instrument(name = "Auto::eval", skip_all)]
- fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
- Ok(Value::Auto)
- }
-}
-
-impl Eval for ast::Bool {
- type Output = Value;
-
- #[tracing::instrument(name = "Bool::eval", skip_all)]
- fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- eval_code(vm, &mut self.exprs())
- }
-}
-
-/// Evaluate a stream of expressions.
-fn eval_code(
- vm: &mut Vm,
- exprs: &mut impl Iterator<Item = ast::Expr>,
-) -> SourceResult<Value> {
- 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<Self::Output> {
- 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::Output> {
- self.expr().eval(vm)
- }
-}
-
-impl Eval for ast::Array {
- type Output = Array;
-
- #[tracing::instrument(skip_all)]
- fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Value>,
- ) -> SourceResult<Value> {
- 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<Value>,
- ) -> SourceResult<Value> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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::<Content>()?.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::<Func>().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<Self::Output> {
- 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<Self::Output> {
- // 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<F>(
- &self,
- vm: &mut Vm,
- value: Array,
- f: F,
- destruct: &ast::Destructuring,
- ) -> SourceResult<Value>
- where
- F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<Value>,
- {
- 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<F>(
- &self,
- vm: &mut Vm,
- dict: Dict,
- f: F,
- destruct: &ast::Destructuring,
- ) -> SourceResult<Value>
- where
- F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<Value>,
- {
- 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<T>(&self, vm: &mut Vm, value: Value, f: T) -> SourceResult<Value>
- where
- T: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<Value>,
- {
- 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<Value> {
- 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<Value> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- if let Some(condition) = self.condition() {
- if !condition.eval(vm)?.cast::<bool>().at(condition.span())? {
- return Ok(Styles::new());
- }
- }
-
- let target = self.target();
- let target = target
- .eval(vm)?
- .cast::<Func>()
- .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<Self::Output> {
- let selector = self
- .selector()
- .map(|sel| sel.eval(vm)?.cast::<ShowableSelector>().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::<Transform>().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<Self::Output> {
- let condition = self.condition();
- if condition.eval(vm)?.cast::<bool>().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<Self::Output> {
- 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::<bool>().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<Self::Output> {
- 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<V: IntoValue>(
- imports: Option<ast::Imports>,
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Module> {
- 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::<PackageSpec>().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<Module> {
- // 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<Module> {
- // 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<Self::Output> {
- 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<Self::Output> {
- 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<Self::Output> {
- 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<Repr>,
-}
-
-/// 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<EcoString>) -> 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<EcoString>) -> 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, "<module {}>", 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<Self> {
- 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<T: Reflect> Reflect for Option<T> {
- fn describe() -> CastInfo {
- T::describe() + NoneValue::describe()
- }
-
- fn castable(value: &Value) -> bool {
- NoneValue::castable(value) || T::castable(value)
- }
-}
-
-impl<T: IntoValue> IntoValue for Option<T> {
- fn into_value(self) -> Value {
- match self {
- Some(v) => v.into_value(),
- None => Value::None,
- }
- }
-}
-
-impl<T: FromValue> FromValue for Option<T> {
- fn from_value(value: Value) -> StrResult<Self> {
- 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<Value> {
- 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<Value> {
- 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<Value> {
- 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<Value> {
- 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::<GenAlign>(), b.downcast::<GenAlign>())
- {
- 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<Value> {
- 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<Value> {
- 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<Value> {
- 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<f64> {
- 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<Length>, b: Rel<Length>) -> StrResult<f64> {
- 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<Value> {
- 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<Value> {
- 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<Value> {
- 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<Value> {
- Ok(Bool(equal(&lhs, &rhs)))
-}
-
-/// Compute whether two values are unequal.
-pub fn neq(lhs: Value, rhs: Value) -> StrResult<Value> {
- 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<Value> {
- 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<Ordering> {
- 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<T: PartialOrd + Debug>(a: &T, b: &T) -> StrResult<Ordering> {
- 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<Value> {
- 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<Value> {
- 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<bool> {
- match (lhs, rhs) {
- (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
- (Dyn(a), Str(b)) => a.downcast::<Regex>().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<Scope>,
- /// 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<EcoString, Slot>, 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<EcoString>, 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<EcoString>, 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<StrResult<&mut Value>> {
- self.0.get_mut(var).map(Slot::write)
- }
-
- /// Iterate over all definitions.
- pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value)> {
- 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> {
- 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> {
- 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<Self> {
- 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<i64>) -> StrResult<Self> {
- 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<Self> {
- 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<i64> {
- 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<Dict> {
- 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<StrPattern>) -> 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<StrPattern>,
- at: Option<StrSide>,
- 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<usize>,
- ) -> SourceResult<Self> {
- // 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<usize>, 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::<Str>().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<Self> {
- 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<Option<usize>> {
- 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<usize> {
- 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::<Array>(),
- }
-}
-
-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<str> for Str {
- fn as_ref(&self) -> &str {
- self
- }
-}
-
-impl Borrow<str> for Str {
- fn borrow(&self) -> &str {
- self
- }
-}
-
-impl From<char> 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<EcoString> for Str {
- fn from(s: EcoString) -> Self {
- Self(s)
- }
-}
-
-impl From<String> for Str {
- fn from(s: String) -> Self {
- Self(s.into())
- }
-}
-
-impl From<Cow<'_, str>> for Str {
- fn from(s: Cow<str>) -> Self {
- Self(s.into())
- }
-}
-
-impl FromIterator<char> for Str {
- fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
- Self(iter.into_iter().collect())
- }
-}
-
-impl From<Str> for EcoString {
- fn from(str: Str) -> Self {
- str.0
- }
-}
-
-impl From<Str> 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<Self> {
- 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<H: Hasher>(&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<Self> {
- 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<Item = (&str, char)> {
- 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<Item = &str> + '_ {
- 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<char> {
- 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<char>),
- 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<Self::Item> {
- 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<Item = (&'a str, char)>,
- modifiers: &str,
-) -> Option<char> {
- 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<Item = &str> {
- 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<Length>),
- /// 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: `<intro>`.
- 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<T>(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::<Length>::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<T: FromValue>(self) -> StrResult<T> {
- T::from_value(self)
- }
-
- /// Try to access a field on the value.
- pub fn field(&self, field: &str) -> StrResult<Value> {
- 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<Ordering> {
- ops::compare(self, other).ok()
- }
-}
-
-impl Hash for Value {
- fn hash<H: Hasher>(&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<dyn Bounds>);
-
-impl Dynamic {
- /// Create a new instance from any value that satisfies the required bounds.
- pub fn new<T>(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<T: Type + 'static>(&self) -> bool {
- (*self.0).as_any().is::<T>()
- }
-
- /// Try to downcast to a reference to a specific type.
- pub fn downcast<T: Type + 'static>(&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<T> 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::<Self>() 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<H: Hasher>(&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<Self> {
- 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<Length>: "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<u16, EcoString>,
-) -> 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<u8>, 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<u8> {
- 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>,
- page_heights: Vec<f32>,
- alloc: Ref,
- page_tree_ref: Ref,
- font_refs: Vec<Ref>,
- image_refs: Vec<Ref>,
- page_refs: Vec<Ref>,
- font_map: Remapper<Font>,
- image_map: Remapper<Image>,
- /// 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<Font, BTreeMap<u16, EcoString>>,
- languages: HashMap<Lang, usize>,
-}
-
-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<u8> {
- 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<T> {
- /// Forwards from the items to the pdf indices.
- to_pdf: HashMap<T, usize>,
- /// Backwards from the pdf indices to the items.
- to_items: Vec<T>,
-}
-
-impl<T> Remapper<T>
-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<Item = (Ref, usize)> + 'a {
- refs.iter().copied().zip(0..self.to_pdf.len())
- }
-
- fn items(&self) -> impl Iterator<Item = &T> + '_ {
- 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<Ref> {
- let mut tree: Vec<HeadingNode> = 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<HeadingNode>,
-}
-
-impl HeadingNode {
- fn leaf(element: Content) -> Self {
- HeadingNode {
- level: element.expect_field::<NonZeroUsize>("level"),
- element,
- children: Vec::new(),
- }
- }
-
- fn len(&self) -> usize {
- 1 + self.children.iter().map(Self::len).sum::<usize>()
- }
-}
-
-/// 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<Ref>,
- 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::<Content>("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::<ColorSpace>().srgb();
- spaces.insert(D65_GRAY).start::<ColorSpace>().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<State>,
- 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<Paint>,
- fill_space: Option<Name<'static>>,
- stroke: Option<Stroke>,
- stroke_space: Option<Name<'static>>,
-}
-
-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<Arc<Bitmap>> {
- 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::<u8, u32>(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<sk::Path> {
- 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<Arc<sk::Pixmap>> {
- 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<Transform> 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<Color> 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<RwLock<Interner>> =
- Lazy::new(|| RwLock::new(Interner { to_id: HashMap::new(), from_id: Vec::new() }));
-
-/// A package-path interner.
-struct Interner {
- to_id: HashMap<Pair, FileId>,
- from_id: Vec<Pair>,
-}
-
-/// An interned pair of a package specification and a path.
-type Pair = &'static (Option<PackageSpec>, 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<PackageSpec>, 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<Self> {
- 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<Self, Self::Err> {
- 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<Self, Self::Err> {
- 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::<u32>()
- .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<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
- s.collect_str(self)
- }
-}
-
-impl<'de> Deserialize<'de> for Version {
- fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
- 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<Self> {
- 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<String, Vec<usize>>,
- /// Metadata about each font in the collection.
- infos: Vec<FontInfo>,
-}
-
-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<Item = &'a Font>) -> 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<Item = (&str, impl Iterator<Item = &FontInfo>)> + '_ {
- // 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<usize> {
- 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<Item = usize> + '_ {
- 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<usize> {
- // 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<Item = usize>,
- ) -> Option<usize> {
- 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(&current.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<Item = FontInfo> + '_ {
- 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<Self> {
- // 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<String> {
- 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<u32>);
-
-impl Coverage {
- /// Encode a vector of codepoints.
- pub fn from_vec(mut codepoints: Vec<u32>) -> 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<Item = u32> + '_ {
- 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::<Vec<_>>(), 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<Repr>);
-
-/// 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<Self> {
- // 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<Item = Self> {
- 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<f64>) -> 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<Em> {
- 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<String> {
- 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<H: Hasher>(&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<f64> for Abs {
- type Output = Self;
-
- fn mul(self, other: f64) -> Self {
- Self(self.0 * other)
- }
-}
-
-impl Mul<Abs> for f64 {
- type Output = Abs;
-
- fn mul(self, other: Abs) -> Abs {
- other * self
- }
-}
-
-impl Div<f64> 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<I: Iterator<Item = Self>>(iter: I) -> Self {
- Self(iter.map(|s| s.0).sum())
- }
-}
-
-impl<'a> Sum<&'a Self> for Abs {
- fn sum<I: Iterator<Item = &'a Self>>(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<Self> = Axes { x: Align::Left, y: Align::Top };
-
- /// Center-horizon alignment.
- pub const CENTER_HORIZON: Axes<Self> = 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<Side> 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<Align> for GenAlign {
- fn from(align: Align) -> Self {
- Self::Specific(align)
- }
-}
-
-impl From<HorizontalAlign> for GenAlign {
- fn from(align: HorizontalAlign) -> Self {
- align.0
- }
-}
-
-impl From<VerticalAlign> 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<GenAlign>: "2d alignment",
-}
-
-cast! {
- Axes<Align>,
- self => self.map(GenAlign::from).into_value(),
-}
-
-cast! {
- Axes<Option<GenAlign>>,
- 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<GenAlign> => aligns.map(Some),
-}
-
-impl From<Axes<GenAlign>> for Axes<Option<GenAlign>> {
- fn from(axes: Axes<GenAlign>) -> Self {
- axes.map(Some)
- }
-}
-
-impl From<Axes<Align>> for Axes<Option<GenAlign>> {
- fn from(axes: Axes<Align>) -> Self {
- axes.map(GenAlign::Specific).into()
- }
-}
-
-impl From<Align> for Axes<Option<GenAlign>> {
- 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<f64> for Angle {
- type Output = Self;
-
- fn mul(self, other: f64) -> Self {
- Self(self.0 * other)
- }
-}
-
-impl Mul<Angle> for f64 {
- type Output = Angle;
-
- fn mul(self, other: Angle) -> Angle {
- other * self
- }
-}
-
-impl Div<f64> 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<I: Iterator<Item = Angle>>(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<T> {
- /// The horizontal component.
- pub x: T,
- /// The vertical component.
- pub y: T,
-}
-
-impl<T> Axes<T> {
- /// 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<F, U>(self, mut f: F) -> Axes<U>
- where
- F: FnMut(T) -> U,
- {
- Axes { x: f(self.x), y: f(self.y) }
- }
-
- /// Convert from `&Axes<T>` to `Axes<&T>`.
- pub fn as_ref(&self) -> Axes<&T> {
- Axes { x: &self.x, y: &self.y }
- }
-
- /// Convert from `&Axes<T>` to `Axes<&<T as Deref>::Target>`.
- pub fn as_deref(&self) -> Axes<&T::Target>
- where
- T: Deref,
- {
- Axes { x: &self.x, y: &self.y }
- }
-
- /// Convert from `&mut Axes<T>` 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<U>(self, other: Axes<U>) -> 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<F>(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<F>(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<bool>) -> Axes<Option<T>> {
- Axes {
- x: if mask.x { Some(self.x) } else { None },
- y: if mask.y { Some(self.y) } else { None },
- }
- }
-}
-
-impl<T: Default> Axes<T> {
- /// 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<T: Ord> Axes<T> {
- /// 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<T> Get<Axis> for Axes<T> {
- 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<T> Debug for Axes<T>
-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::<GenAlign>())
- {
- write!(f, "{:?} + {:?}", x, y)
- } else if (&self.x as &dyn Any).is::<Abs>() {
- 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<T> Axes<Option<T>> {
- /// Unwrap the individual fields.
- pub fn unwrap_or(self, other: Axes<T>) -> Axes<T> {
- Axes {
- x: self.x.unwrap_or(other.x),
- y: self.y.unwrap_or(other.y),
- }
- }
-}
-
-impl<T> Axes<Smart<T>> {
- /// Unwrap the individual fields.
- pub fn unwrap_or(self, other: Axes<T>) -> Axes<T> {
- Axes {
- x: self.x.unwrap_or(other.x),
- y: self.y.unwrap_or(other.y),
- }
- }
-}
-
-impl Axes<bool> {
- /// Select `t.x` if `self.x` is true and `f.x` otherwise and same for `y`.
- pub fn select<T>(self, t: Axes<T>, f: Axes<T>) -> Axes<T> {
- Axes {
- x: if self.x { t.x } else { f.x },
- y: if self.y { t.y } else { f.y },
- }
- }
-}
-
-impl Not for Axes<bool> {
- type Output = Self;
-
- fn not(self) -> Self::Output {
- Self { x: !self.x, y: !self.y }
- }
-}
-
-impl BitOr for Axes<bool> {
- type Output = Self;
-
- fn bitor(self, rhs: Self) -> Self::Output {
- Self { x: self.x | rhs.x, y: self.y | rhs.y }
- }
-}
-
-impl BitOr<bool> for Axes<bool> {
- type Output = Self;
-
- fn bitor(self, rhs: bool) -> Self::Output {
- Self { x: self.x | rhs, y: self.y | rhs }
- }
-}
-
-impl BitAnd for Axes<bool> {
- type Output = Self;
-
- fn bitand(self, rhs: Self) -> Self::Output {
- Self { x: self.x & rhs.x, y: self.y & rhs.y }
- }
-}
-
-impl BitAnd<bool> for Axes<bool> {
- type Output = Self;
-
- fn bitand(self, rhs: bool) -> Self::Output {
- Self { x: self.x & rhs, y: self.y & rhs }
- }
-}
-
-impl BitOrAssign for Axes<bool> {
- fn bitor_assign(&mut self, rhs: Self) {
- self.x |= rhs.x;
- self.y |= rhs.y;
- }
-}
-
-impl BitAndAssign for Axes<bool> {
- fn bitand_assign(&mut self, rhs: Self) {
- self.x &= rhs.x;
- self.y &= rhs.y;
- }
-}
-
-cast! {
- Axes<Rel<Length>>,
- 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<T: Resolve> Resolve for Axes<T> {
- type Output = Axes<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Fold> Fold for Axes<Option<T>> {
- type Output = Axes<T::Output>;
-
- 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<LumaColor> 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<Self, Self::Err> {
- 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<T: Into<RgbaColor>> From<T> 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<CmykColor> 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<T> {
- /// 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<T> Corners<T> {
- /// 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<F, U>(self, mut f: F) -> Corners<U>
- 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<U>(self, other: Corners<U>) -> 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<Item = &T> {
- [&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<T> Get<Corner> for Corners<T> {
- 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<T: Reflect> Reflect for Corners<Option<T>> {
- fn describe() -> CastInfo {
- T::describe() + Dict::describe()
- }
-
- fn castable(value: &Value) -> bool {
- Dict::castable(value) || T::castable(value)
- }
-}
-
-impl<T> IntoValue for Corners<T>
-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<T> FromValue for Corners<Option<T>>
-where
- T: FromValue + Clone,
-{
- fn from_value(mut value: Value) -> StrResult<Self> {
- 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<T: Resolve> Resolve for Corners<T> {
- type Output = Corners<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Fold> Fold for Corners<Option<T>> {
- type Output = Corners<T::Output>;
-
- 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<Paint>, stroke: Option<Stroke>) -> 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<f64>, 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<f64> for Em {
- type Output = Self;
-
- fn mul(self, other: f64) -> Self {
- Self(self.0 * other)
- }
-}
-
-impl Mul<Em> for f64 {
- type Output = Em;
-
- fn mul(self, other: Em) -> Em {
- other * self
- }
-}
-
-impl Div<f64> 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<I: Iterator<Item = Self>>(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<f64> for Fr {
- type Output = Self;
-
- fn mul(self, other: f64) -> Self {
- Self(self.0 * other)
- }
-}
-
-impl Mul<Fr> for f64 {
- type Output = Fr;
-
- fn mul(self, other: Fr) -> Fr {
- other * self
- }
-}
-
-impl Div<f64> 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<I: Iterator<Item = Self>>(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> {
- (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<f64> {
- 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<Ordering> {
- 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<Abs> for Length {
- fn from(abs: Abs) -> Self {
- Self { abs, em: Em::zero() }
- }
-}
-
-impl From<Em> 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<f64> for Length {
- type Output = Self;
-
- fn mul(self, rhs: f64) -> Self::Output {
- Self { abs: self.abs * rhs, em: self.em * rhs }
- }
-}
-
-impl Div<f64> 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<Index> {
- /// 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<Output = Self>
- + Add<Output = Self>
- + Sub<Output = Self>
- + Mul<f64, Output = Self>
- + Div<f64, Output = Self>
-{
- /// 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<T: Into<Color>> From<T> 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<PathItem>);
-
-/// 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<Axis> 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<f64> for Point {
- type Output = Self;
-
- fn mul(self, other: f64) -> Self {
- Self { x: self.x * other, y: self.y * other }
- }
-}
-
-impl Mul<Point> for f64 {
- type Output = Point;
-
- fn mul(self, other: Point) -> Point {
- other * self
- }
-}
-
-impl Div<f64> 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<T: Numeric>(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<f64> for Ratio {
- type Output = Self;
-
- fn mul(self, other: f64) -> Self {
- Self(self.0 * other)
- }
-}
-
-impl Mul<Ratio> for f64 {
- type Output = Ratio;
-
- fn mul(self, other: Ratio) -> Ratio {
- other * self
- }
-}
-
-impl Div<f64> for Ratio {
- type Output = Self;
-
- fn div(self, other: f64) -> Self {
- Self(self.0 / other)
- }
-}
-
-impl Div<Ratio> 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<T: Numeric> {
- /// The relative part.
- pub rel: Ratio,
- /// The absolute part.
- pub abs: T,
-}
-
-impl<T: Numeric> Rel<T> {
- /// 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<F, U>(self, f: F) -> Rel<U>
- where
- F: FnOnce(T) -> U,
- U: Numeric,
- {
- Rel { rel: self.rel, abs: f(self.abs) }
- }
-}
-
-impl Rel<Length> {
- /// Try to divide two relative lengths.
- pub fn try_div(self, other: Self) -> Option<f64> {
- 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<T: Numeric> Debug for Rel<T> {
- 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<Abs> for Rel<Length> {
- fn from(abs: Abs) -> Self {
- Rel::from(Length::from(abs))
- }
-}
-
-impl From<Em> for Rel<Length> {
- fn from(em: Em) -> Self {
- Rel::from(Length::from(em))
- }
-}
-
-impl<T: Numeric> From<T> for Rel<T> {
- fn from(abs: T) -> Self {
- Self { rel: Ratio::zero(), abs }
- }
-}
-
-impl<T: Numeric> From<Ratio> for Rel<T> {
- fn from(rel: Ratio) -> Self {
- Self { rel, abs: T::zero() }
- }
-}
-
-impl<T: Numeric + PartialOrd> PartialOrd for Rel<T> {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- 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<T: Numeric> Neg for Rel<T> {
- type Output = Self;
-
- fn neg(self) -> Self {
- Self { rel: -self.rel, abs: -self.abs }
- }
-}
-
-impl<T: Numeric> Add for Rel<T> {
- type Output = Self;
-
- fn add(self, other: Self) -> Self::Output {
- Self {
- rel: self.rel + other.rel,
- abs: self.abs + other.abs,
- }
- }
-}
-
-impl<T: Numeric> Sub for Rel<T> {
- type Output = Self;
-
- fn sub(self, other: Self) -> Self::Output {
- self + -other
- }
-}
-
-impl<T: Numeric> Mul<f64> for Rel<T> {
- type Output = Self;
-
- fn mul(self, other: f64) -> Self::Output {
- Self { rel: self.rel * other, abs: self.abs * other }
- }
-}
-
-impl<T: Numeric> Mul<Rel<T>> for f64 {
- type Output = Rel<T>;
-
- fn mul(self, other: Rel<T>) -> Self::Output {
- other * self
- }
-}
-
-impl<T: Numeric> Div<f64> for Rel<T> {
- type Output = Self;
-
- fn div(self, other: f64) -> Self::Output {
- Self { rel: self.rel / other, abs: self.abs / other }
- }
-}
-
-impl<T: Numeric + AddAssign> AddAssign for Rel<T> {
- fn add_assign(&mut self, other: Self) {
- self.rel += other.rel;
- self.abs += other.abs;
- }
-}
-
-impl<T: Numeric + SubAssign> SubAssign for Rel<T> {
- fn sub_assign(&mut self, other: Self) {
- self.rel -= other.rel;
- self.abs -= other.abs;
- }
-}
-
-impl<T: Numeric + MulAssign<f64>> MulAssign<f64> for Rel<T> {
- fn mul_assign(&mut self, other: f64) {
- self.rel *= other;
- self.abs *= other;
- }
-}
-
-impl<T: Numeric + DivAssign<f64>> DivAssign<f64> for Rel<T> {
- fn div_assign(&mut self, other: f64) {
- self.rel /= other;
- self.abs /= other;
- }
-}
-
-impl<T: Numeric> Add<T> for Ratio {
- type Output = Rel<T>;
-
- fn add(self, other: T) -> Self::Output {
- Rel::from(self) + Rel::from(other)
- }
-}
-
-impl<T: Numeric> Add<T> for Rel<T> {
- type Output = Self;
-
- fn add(self, other: T) -> Self::Output {
- self + Rel::from(other)
- }
-}
-
-impl<T: Numeric> Add<Ratio> for Rel<T> {
- type Output = Self;
-
- fn add(self, other: Ratio) -> Self::Output {
- self + Rel::from(other)
- }
-}
-
-impl<T> Resolve for Rel<T>
-where
- T: Resolve + Numeric,
- <T as Resolve>::Output: Numeric,
-{
- type Output = Rel<<T as Resolve>::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|abs| abs.resolve(styles))
- }
-}
-
-impl Fold for Rel<Abs> {
- type Output = Self;
-
- fn fold(self, _: Self::Output) -> Self::Output {
- self
- }
-}
-
-impl Fold for Rel<Length> {
- type Output = Self;
-
- fn fold(self, _: Self::Output) -> Self::Output {
- self
- }
-}
-
-cast! {
- Rel<Abs>,
- 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<Abs>,
- fill: Option<Paint>,
- stroke: Sides<Option<Stroke>>,
-) -> Vec<Shape> {
- 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<Abs>) -> 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<Abs>,
- stroke: Sides<Option<Stroke>>,
-) -> Vec<(Path, Option<Stroke>)> {
- 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<f64> for Scalar {
- fn from(float: f64) -> Self {
- Self(float)
- }
-}
-
-impl From<Scalar> 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<f64> 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<Ordering> {
- 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<H: Hasher>(&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<T: Into<Self>> Add<T> for Scalar {
- type Output = Self;
-
- fn add(self, rhs: T) -> Self::Output {
- Self(self.0 + rhs.into().0)
- }
-}
-
-impl<T: Into<Self>> AddAssign<T> for Scalar {
- fn add_assign(&mut self, rhs: T) {
- self.0 += rhs.into().0;
- }
-}
-
-impl<T: Into<Self>> Sub<T> for Scalar {
- type Output = Self;
-
- fn sub(self, rhs: T) -> Self::Output {
- Self(self.0 - rhs.into().0)
- }
-}
-
-impl<T: Into<Self>> SubAssign<T> for Scalar {
- fn sub_assign(&mut self, rhs: T) {
- self.0 -= rhs.into().0;
- }
-}
-
-impl<T: Into<Self>> Mul<T> for Scalar {
- type Output = Self;
-
- fn mul(self, rhs: T) -> Self::Output {
- Self(self.0 * rhs.into().0)
- }
-}
-
-impl<T: Into<Self>> MulAssign<T> for Scalar {
- fn mul_assign(&mut self, rhs: T) {
- self.0 *= rhs.into().0;
- }
-}
-
-impl<T: Into<Self>> Div<T> for Scalar {
- type Output = Self;
-
- fn div(self, rhs: T) -> Self::Output {
- Self(self.0 / rhs.into().0)
- }
-}
-
-impl<T: Into<Self>> DivAssign<T> for Scalar {
- fn div_assign(&mut self, rhs: T) {
- self.0 /= rhs.into().0;
- }
-}
-
-impl<T: Into<Self>> Rem<T> for Scalar {
- type Output = Self;
-
- fn rem(self, rhs: T) -> Self::Output {
- Self(self.0 % rhs.into().0)
- }
-}
-
-impl<T: Into<Self>> RemAssign<T> for Scalar {
- fn rem_assign(&mut self, rhs: T) {
- self.0 %= rhs.into().0;
- }
-}
-
-impl Sum for Scalar {
- fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
- Self(iter.map(|s| s.0).sum())
- }
-}
-
-impl<'a> Sum<&'a Self> for Scalar {
- fn sum<I: Iterator<Item = &'a Self>>(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<Paint>,
- /// The shape's border stroke.
- pub stroke: Option<Stroke>,
-}
-
-/// 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<T> {
- /// 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<T> Sides<T> {
- /// 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<F, U>(self, mut f: F) -> Sides<U>
- 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<U>(self, other: Sides<U>) -> 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<Item = &T> {
- [&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<T: Add> Sides<T> {
- /// Sums up `left` and `right` into `x`, and `top` and `bottom` into `y`.
- pub fn sum_by_axis(self) -> Axes<T::Output> {
- Axes::new(self.left + self.right, self.top + self.bottom)
- }
-}
-
-impl Sides<Rel<Abs>> {
- /// Evaluate the sides relative to the given `size`.
- pub fn relative_to(self, size: Size) -> Sides<Abs> {
- 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<T> Get<Side> for Sides<T> {
- 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<T: Reflect> Reflect for Sides<Option<T>> {
- fn describe() -> CastInfo {
- T::describe() + Dict::describe()
- }
-
- fn castable(value: &Value) -> bool {
- Dict::castable(value) || T::castable(value)
- }
-}
-
-impl<T> IntoValue for Sides<T>
-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<T> FromValue for Sides<Option<T>>
-where
- T: Default + FromValue + Clone,
-{
- fn from_value(mut value: Value) -> StrResult<Self> {
- 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<T: Resolve> Resolve for Sides<T> {
- type Output = Sides<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T: Fold> Fold for Sides<Option<T>> {
- type Output = Sides<T::Output>;
-
- 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<Abs>;
-
-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<f64> for Size {
- type Output = Self;
-
- fn mul(self, other: f64) -> Self {
- Self { x: self.x * other, y: self.y * other }
- }
-}
-
-impl Mul<Size> for f64 {
- type Output = Size;
-
- fn mul(self, other: Size) -> Size {
- other * self
- }
-}
-
-impl Div<f64> 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<T> {
- /// The value should be determined smartly based on the circumstances.
- Auto,
- /// A specific value.
- Custom(T),
-}
-
-impl<T> Smart<T> {
- /// 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<T> {
- match self {
- Self::Auto => None,
- Self::Custom(x) => Some(x),
- }
- }
-
- /// Map the contained custom value with `f`.
- pub fn map<F, U>(self, f: F) -> Smart<U>
- 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<F, U>(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<T>) -> 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<F>(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<T> Default for Smart<T> {
- fn default() -> Self {
- Self::Auto
- }
-}
-
-impl<T: Reflect> Reflect for Smart<T> {
- fn castable(value: &Value) -> bool {
- AutoValue::castable(value) || T::castable(value)
- }
-
- fn describe() -> CastInfo {
- T::describe() + AutoValue::describe()
- }
-}
-
-impl<T: IntoValue> IntoValue for Smart<T> {
- fn into_value(self) -> Value {
- match self {
- Smart::Custom(v) => v.into_value(),
- Smart::Auto => Value::Auto,
- }
- }
-}
-
-impl<T: FromValue> FromValue for Smart<T> {
- fn from_value(value: Value) -> StrResult<Self> {
- match value {
- Value::Auto => Ok(Self::Auto),
- v if T::castable(&v) => Ok(Self::Custom(T::from_value(v)?)),
- _ => Err(Self::error(&value)),
- }
- }
-}
-
-impl<T: Resolve> Resolve for Smart<T> {
- type Output = Smart<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-impl<T> Fold for Smart<T>
-where
- T: Fold,
- T::Output: Default,
-{
- type Output = Smart<T::Output>;
-
- 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<DashPattern<Abs, Abs>>,
- /// 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<T = Length> {
- /// The stroke's paint.
- pub paint: Smart<Paint>,
- /// The stroke's thickness.
- pub thickness: Smart<T>,
- /// The stroke's line cap.
- pub line_cap: Smart<LineCap>,
- /// The stroke's line join.
- pub line_join: Smart<LineJoin>,
- /// The stroke's line dash pattern.
- pub dash_pattern: Smart<Option<DashPattern<T>>>,
- /// The miter limit.
- pub miter_limit: Smart<Scalar>,
-}
-
-impl<T> PartialStroke<T> {
- /// Map the contained lengths with `f`.
- pub fn map<F, U>(self, f: F) -> PartialStroke<U>
- 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<Abs> {
- /// 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<T: Debug> Debug for PartialStroke<T> {
- 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<Abs>;
-
- 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<Abs> {
- 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<T: FromValue>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> {
- Ok(dict.take(key).ok().map(T::from_value)
- .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto))
- }
-
- let paint = take::<Paint>(&mut dict, "paint")?;
- let thickness = take::<Length>(&mut dict, "thickness")?;
- let line_cap = take::<LineCap>(&mut dict, "cap")?;
- let line_join = take::<LineJoin>(&mut dict, "join")?;
- let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?;
- let miter_limit = take::<f64>(&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<Abs>,
- 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<T = Length, DT = DashLength<T>> {
- /// The dash array.
- pub array: Vec<DT>,
- /// The dash phase.
- pub phase: T,
-}
-
-impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> {
- 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<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> {
- fn from(array: Vec<DashLength<T>>) -> Self {
- Self { array, phase: T::default() }
- }
-}
-
-impl Resolve for DashPattern {
- type Output = DashPattern<Abs>;
-
- 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<DashLength> => Self { array, phase: Length::zero() },
- mut dict: Dict => {
- let array: Vec<DashLength> = 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<T = Length> {
- LineWidth,
- Length(T),
-}
-
-impl From<Abs> for DashLength {
- fn from(l: Abs) -> Self {
- DashLength::Length(l.into())
- }
-}
-
-impl<T> DashLength<T> {
- fn finish(self, line_width: T) -> T {
- match self {
- Self::LineWidth => line_width,
- Self::Length(l) => l,
- }
- }
-}
-
-impl Resolve for DashLength {
- type Output = DashLength<Abs>;
-
- 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<Value> {
- match node.cast::<ast::Expr>() {
- 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<Module> {
- 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<EcoString>)>, 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<Completion>)> {
- 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<EcoString>,
- /// An optional short description, at most one sentence.
- pub detail: Option<EcoString>,
-}
-
-/// 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::<ast::Expr>();
- 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::<ast::Expr>();
- 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::<ast::Expr>());
- 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::<ast::Expr>());
- 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::<ast::Args>();
- if let Some(grand) = parent.parent();
- if let Some(expr) = grand.cast::<ast::Expr>();
- 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::<ast::Ident>();
- then {
- if let Some(next) = deciding.next_leaf() {
- ctx.from = ctx.cursor.min(next.offset());
- }
-
- named_param_value_completions(ctx, &callee, &param);
- 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(&param.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(&param.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<Completion>,
- seen_casts: HashSet<u128>,
-}
-
-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<Self> {
- 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<EcoString>,
- 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::<ast::LetBinding>() {
- 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::<ast::ForLoop>() {
- 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<Tag> {
- 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<Tag> {
- // 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<Tag> {
- let next = node.next_sibling()?;
- let expr = next.cast::<ast::Expr>()?;
- 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("<code>");
- let node = LinkedNode::new(root);
- highlight_html_impl(&mut buf, &node);
- buf.push_str("</code>");
- 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("<span class=\"");
- html.push_str(tag.css_class());
- html.push_str("\">");
- }
- }
-
- let text = node.text();
- if !text.is_empty() {
- for c in text.chars() {
- match c {
- '<' => html.push_str("&lt;"),
- '>' => html.push_str("&gt;"),
- '&' => html.push_str("&amp;"),
- '\'' => html.push_str("&#39;"),
- '"' => html.push_str("&quot;"),
- _ => html.push(c),
- }
- }
- } else {
- for child in node.children() {
- highlight_html_impl(html, &child);
- }
- }
-
- if span {
- html.push_str("</span>");
- }
-}
-
-#[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<usize>, 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<usize>, 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<Self> {
- 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<Jump> {
- 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<Position> {
- 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<Point> {
- 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<Item = &'a FontInfo>) -> 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<Tooltip> {
- 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<Tooltip> {
- let mut ancestor = leaf;
- while !ancestor.is::<ast::Expr>() {
- ancestor = ancestor.parent()?;
- }
-
- let expr = ancestor.cast::<ast::Expr>()?;
- 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<EcoString> = 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<Tooltip> {
- 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<Tooltip> {
- 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<Tooltip> {
- 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::<ast::Named>();
- 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::<ast::Expr>();
- 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::<ast::Ident>();
- 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::<ast::Str>();
- if let Some(param) = info.param(&named.name());
- if let Some(docs) = find_string_doc(&param.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<Tooltip> {
- if_chain! {
- // Ensure that we are on top of a string.
- if let Some(string) = leaf.cast::<ast::Str>();
- 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::<ast::Named>();
- 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<Prehashed<Repr>>);
-
-/// 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<u32>,
- /// A loader for fonts referenced by an image (currently, only applies to
- /// SVG).
- loader: PreparedLoader,
- /// A text describing the image.
- alt: Option<EcoString>,
-}
-
-impl Image {
- /// Create an image from a buffer and a format.
- #[comemo::memoize]
- pub fn new(
- data: Bytes,
- format: ImageFormat,
- alt: Option<EcoString>,
- ) -> StrResult<Self> {
- 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<dyn World + '_>,
- fallback_family: Option<&str>,
- alt: Option<EcoString>,
- ) -> StrResult<Self> {
- 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<u32> {
- 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<DecodedImage> {
- 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<RasterFormat> 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<ttf_parser::RasterImageFormat> for RasterFormat {
- fn from(format: ttf_parser::RasterImageFormat) -> Self {
- match format {
- ttf_parser::RasterImageFormat::PNG => RasterFormat::Png,
- }
- }
-}
-
-impl From<ttf_parser::RasterImageFormat> 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<IccProfile>, RasterFormat),
- /// An decoded SVG tree.
- Svg(usvg::Tree),
-}
-
-impl DecodedImage {
- /// The size of the image in pixels.
- pub fn size(&self) -> Axes<u32> {
- 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<u8>);
-
-/// Decode a raster image.
-#[comemo::memoize]
-fn decode_raster(data: &Bytes, format: RasterFormat) -> StrResult<Arc<DecodedImage>> {
- fn decode_with<'a, T: ImageDecoder<'a>>(
- decoder: ImageResult<T>,
- ) -> ImageResult<(image::DynamicImage, Option<IccProfile>)> {
- 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<dyn SvgFontLoader + '_>,
-) -> StrResult<Arc<DecodedImage>> {
- // 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<dyn SvgFontLoader + '_>,
-) -> fontdb::Database {
- let mut referenced = BTreeMap::<EcoString, bool>::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<F>(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<Font>;
-
- /// 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<BTreeMap<EcoString, EcoVec<Font>>>,
- fallback_family_cased: Option<String>,
-}
-
-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<Font> {
- 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<EcoString, EcoVec<Font>>,
- fallback_family_cased: Option<String>,
-}
-
-impl SvgFontLoader for PreparedLoader {
- fn load(&self, family: &str) -> EcoVec<Font> {
- 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<Document> {
- 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<Library>;
-
- /// Metadata about all known fonts.
- fn book(&self) -> &Prehashed<FontBook>;
-
- /// 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<Source>;
-
- /// Try to access the specified file.
- fn file(&self, id: FileId) -> FileResult<Bytes>;
-
- /// Try to access the font with the given index in the font book.
- fn font(&self, index: usize) -> Option<Font>;
-
- /// 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<i64>) -> Option<Datetime>;
-
- /// 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<EcoString>)] {
- &[]
- }
-}
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<Attr>,
-}
-
-/// Attributes that can be attached to content.
-#[derive(Debug, Clone, PartialEq, Hash)]
-enum Attr {
- Span(Span),
- Field(EcoString),
- Value(Prehashed<Value>),
- Child(Prehashed<Content>),
- 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<Item = Self>) -> 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::<SequenceElem>() && self.attrs.is_empty()
- }
-
- /// Whether the contained element is of type `T`.
- pub fn is<T: Element>(&self) -> bool {
- self.func == T::func()
- }
-
- /// Cast to `T` if the contained element is of type `T`.
- pub fn to<T: Element>(&self) -> Option<&T> {
- T::unpack(self)
- }
-
- /// Access the children if this is a sequence.
- pub fn to_sequence(&self) -> Option<impl Iterator<Item = &Self>> {
- if !self.is::<SequenceElem>() {
- 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::<StyledElem>() {
- 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<C>(&self) -> bool
- where
- C: ?Sized + 'static,
- {
- (self.func.0.vtable)(TypeId::of::<C>()).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<C>(&self) -> Option<&C>
- where
- C: ?Sized + 'static,
- {
- let vtable = (self.func.0.vtable)(TypeId::of::<C>())?;
- 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<C>(&mut self) -> Option<&mut C>
- where
- C: ?Sized + 'static,
- {
- let vtable = (self.func.0.vtable)(TypeId::of::<C>())?;
- 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<EcoString>,
- 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<EcoString>, 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<Value> {
- 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<Item = (&EcoString, Value)> {
- 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<Item = (&EcoString, &Value)> {
- 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<T: FromValue>(&self, name: &str) -> Option<T> {
- 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<T: FromValue>(&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<Value>) -> StrResult<Value> {
- 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<Style>) -> Self {
- if self.is::<StyledElem>() {
- let prev =
- self.attrs.make_mut().iter_mut().find_map(Attr::styles_mut).unwrap();
- prev.apply_one(style.into());
- self
- } else {
- self.styled_with_map(style.into().into())
- }
- }
-
- /// Style this content with a full style map.
- pub fn styled_with_map(mut self, styles: Styles) -> Self {
- if styles.is_empty() {
- return self;
- }
-
- if self.is::<StyledElem>() {
- let prev =
- self.attrs.make_mut().iter_mut().find_map(Attr::styles_mut).unwrap();
- prev.apply(styles);
- self
- } else {
- let mut content = Content::new(StyledElem::func());
- content.attrs.push(Attr::Child(Prehashed::new(self)));
- content.attrs.push(Attr::Styles(styles));
- content
- }
- }
-
- /// Style this content with a recipe, eagerly applying it if possible.
- pub fn styled_with_recipe(self, vm: &mut Vm, recipe: Recipe) -> SourceResult<Self> {
- if recipe.selector.is_none() {
- recipe.apply_vm(vm, self)
- } else {
- Ok(self.styled(recipe))
- }
- }
-
- /// Repeat this content `count` times.
- pub fn repeat(&self, count: usize) -> Self {
- Self::sequence(vec![self.clone(); count])
- }
-
- /// Disable a show rule recipe.
- pub fn guarded(mut self, guard: Guard) -> Self {
- self.attrs.push(Attr::Guard(guard));
- self
- }
-
- /// Check whether a show rule recipe is disabled.
- pub fn is_guarded(&self, guard: Guard) -> bool {
- self.attrs.contains(&Attr::Guard(guard))
- }
-
- /// Whether no show rule was executed for this content so far.
- pub fn is_pristine(&self) -> bool {
- !self.attrs.iter().any(|modifier| matches!(modifier, Attr::Guard(_)))
- }
-
- /// Whether this content has already been prepared.
- pub fn is_prepared(&self) -> bool {
- self.attrs.contains(&Attr::Prepared)
- }
-
- /// Mark this content as prepared.
- pub fn mark_prepared(&mut self) {
- self.attrs.push(Attr::Prepared);
- }
-
- /// Whether the content needs to be realized specially.
- pub fn needs_preparation(&self) -> bool {
- (self.can::<dyn Locatable>()
- || self.can::<dyn Synthesize>()
- || self.label().is_some())
- && !self.is_prepared()
- }
-
- /// This content's location in the document flow.
- pub fn location(&self) -> Option<Location> {
- self.attrs.iter().find_map(|modifier| match modifier {
- Attr::Location(location) => Some(*location),
- _ => None,
- })
- }
-
- /// Attach a location to this content.
- pub fn set_location(&mut self, location: Location) {
- self.attrs.push(Attr::Location(location));
- }
-
- /// Queries the content tree for all elements that match the given selector.
- ///
- /// Elements produced in `show` rules will not be included in the results.
- #[tracing::instrument(skip_all)]
- pub fn query(&self, selector: Selector) -> Vec<&Content> {
- let mut results = Vec::new();
- self.traverse(&mut |element| {
- if selector.matches(element) {
- results.push(element);
- }
- });
- results
- }
-
- /// Queries the content tree for the first element that match the given
- /// selector.
- ///
- /// Elements produced in `show` rules will not be included in the results.
- #[tracing::instrument(skip_all)]
- pub fn query_first(&self, selector: Selector) -> Option<&Content> {
- let mut result = None;
- self.traverse(&mut |element| {
- if result.is_none() && selector.matches(element) {
- result = Some(element);
- }
- });
- result
- }
-
- /// Extracts the plain text of this content.
- pub fn plain_text(&self) -> EcoString {
- let mut text = EcoString::new();
- self.traverse(&mut |element| {
- if let Some(textable) = element.with::<dyn PlainText>() {
- textable.plain_text(&mut text);
- }
- });
- text
- }
-
- /// Traverse this content.
- fn traverse<'a, F>(&'a self, f: &mut F)
- where
- F: FnMut(&'a Content),
- {
- f(self);
-
- for attr in &self.attrs {
- match attr {
- Attr::Child(child) => child.traverse(f),
- Attr::Value(value) => walk_value(value, f),
- _ => {}
- }
- }
-
- /// Walks a given value to find any content that matches the selector.
- fn walk_value<'a, F>(value: &'a Value, f: &mut F)
- where
- F: FnMut(&'a Content),
- {
- match value {
- Value::Content(content) => content.traverse(f),
- Value::Array(array) => {
- for value in array {
- walk_value(value, f);
- }
- }
- _ => {}
- }
- }
- }
-}
-
-impl Debug for Content {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- let name = self.func.name();
- if let Some(text) = item!(text_str)(self) {
- f.write_char('[')?;
- f.write_str(&text)?;
- f.write_char(']')?;
- return Ok(());
- } else if name == "space" {
- return f.write_str("[ ]");
- }
-
- let mut pieces: Vec<_> = self
- .fields()
- .map(|(name, value)| eco_format!("{name}: {value:?}"))
- .collect();
-
- if self.is::<StyledElem>() {
- pieces.push(EcoString::from(".."));
- }
-
- f.write_str(name)?;
- f.write_str(&pretty_array_like(&pieces, false))
- }
-}
-
-impl Default for Content {
- fn default() -> Self {
- Self::empty()
- }
-}
-
-impl PartialEq for Content {
- fn eq(&self, other: &Self) -> bool {
- if let (Some(left), Some(right)) = (self.to_sequence(), other.to_sequence()) {
- left.eq(right)
- } else if let (Some(left), Some(right)) = (self.to_styled(), other.to_styled()) {
- left == right
- } else {
- self.func == other.func && self.fields_ref().eq(other.fields_ref())
- }
- }
-}
-
-impl Add for Content {
- type Output = Self;
-
- fn add(self, mut rhs: Self) -> Self::Output {
- let mut lhs = self;
- match (lhs.is::<SequenceElem>(), rhs.is::<SequenceElem>()) {
- (true, true) => {
- lhs.attrs.extend(rhs.attrs);
- lhs
- }
- (true, false) => {
- lhs.attrs.push(Attr::Child(Prehashed::new(rhs)));
- lhs
- }
- (false, true) => {
- rhs.attrs.insert(0, Attr::Child(Prehashed::new(lhs)));
- rhs
- }
- (false, false) => Self::sequence([lhs, rhs]),
- }
- }
-}
-
-impl AddAssign for Content {
- fn add_assign(&mut self, rhs: Self) {
- *self = std::mem::take(self) + rhs;
- }
-}
-
-impl Sum for Content {
- fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
- Self::sequence(iter)
- }
-}
-
-impl Attr {
- fn child(&self) -> Option<&Content> {
- match self {
- Self::Child(child) => Some(child),
- _ => None,
- }
- }
-
- fn styles(&self) -> Option<&Styles> {
- match self {
- Self::Styles(styles) => Some(styles),
- _ => None,
- }
- }
-
- fn styles_mut(&mut self) -> Option<&mut Styles> {
- match self {
- Self::Styles(styles) => Some(styles),
- _ => None,
- }
- }
-
- fn field(&self) -> Option<&EcoString> {
- match self {
- Self::Field(field) => Some(field),
- _ => None,
- }
- }
-
- fn value(&self) -> Option<&Value> {
- match self {
- Self::Value(value) => Some(value),
- _ => None,
- }
- }
-
- fn span(&self) -> Option<Span> {
- match self {
- Self::Span(span) => Some(*span),
- _ => None,
- }
- }
-}
-
-/// Display: Sequence
-/// Category: special
-#[element]
-struct SequenceElem {}
-
-/// Display: Sequence
-/// Category: special
-#[element]
-struct StyledElem {}
-
-/// Hosts metadata and ensures metadata is produced even for empty elements.
-///
-/// Display: Meta
-/// Category: special
-#[element(Behave)]
-pub struct MetaElem {
- /// Metadata that should be attached to all elements affected by this style
- /// property.
- #[fold]
- pub data: Vec<Meta>,
-}
-
-impl Behave for MetaElem {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Ignorant
- }
-}
-
-impl Fold for Vec<Meta> {
- type Output = Self;
-
- fn fold(mut self, outer: Self::Output) -> Self::Output {
- self.extend(outer);
- self
- }
-}
-
-/// Tries to extract the plain-text representation of the element.
-pub trait PlainText {
- /// Write this element's plain text into the given buffer.
- fn plain_text(&self, text: &mut EcoString);
-}
-
-/// The missing key access error message when no default value was given.
-#[cold]
-fn missing_field_no_default(key: &str) -> EcoString {
- eco_format!(
- "content does not contain field {:?} and \
- no default value was specified",
- Str::from(key)
- )
-}
diff --git a/src/model/element.rs b/src/model/element.rs
deleted file mode 100644
index c673ee41..00000000
--- a/src/model/element.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-use std::any::TypeId;
-use std::fmt::{self, Debug, Formatter};
-use std::hash::{Hash, Hasher};
-
-use once_cell::sync::Lazy;
-
-use super::{Content, Selector, Styles};
-use crate::diag::SourceResult;
-use crate::eval::{cast, Args, Dict, Func, FuncInfo, Value, Vm};
-
-/// A document element.
-pub trait Element: Construct + Set + Sized + 'static {
- /// Pack the element into type-erased content.
- fn pack(self) -> Content;
-
- /// Extract this element from type-erased content.
- fn unpack(content: &Content) -> Option<&Self>;
-
- /// The element's function.
- fn func() -> ElemFunc;
-}
-
-/// An element's constructor function.
-pub trait Construct {
- /// Construct an element from the arguments.
- ///
- /// This is passed only the arguments that remain after execution of the
- /// element's set rule.
- fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content>;
-}
-
-/// An element's set rule.
-pub trait Set {
- /// Parse relevant arguments into style properties for this element.
- fn set(args: &mut Args) -> SourceResult<Styles>;
-}
-
-/// An element's function.
-#[derive(Copy, Clone)]
-pub struct ElemFunc(pub(super) &'static NativeElemFunc);
-
-impl ElemFunc {
- /// The function's name.
- pub fn name(self) -> &'static str {
- self.0.name
- }
-
- /// Apply the given arguments to the function.
- pub fn with(self, args: Args) -> Func {
- Func::from(self).with(args)
- }
-
- /// Extract details about the function.
- pub fn info(&self) -> &'static FuncInfo {
- &self.0.info
- }
-
- /// Construct an element.
- pub fn construct(self, vm: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- (self.0.construct)(vm, args)
- }
-
- /// Whether the contained element has the given capability.
- pub fn can<C>(&self) -> bool
- where
- C: ?Sized + 'static,
- {
- (self.0.vtable)(TypeId::of::<C>()).is_some()
- }
-
- /// Create a selector for elements of this function.
- pub fn select(self) -> Selector {
- Selector::Elem(self, None)
- }
-
- /// Create a selector for elements of this function, filtering for those
- /// whose [fields](super::Content::field) match the given arguments.
- pub fn where_(self, fields: Dict) -> Selector {
- Selector::Elem(self, Some(fields))
- }
-
- /// Execute the set rule for the element and return the resulting style map.
- pub fn set(self, mut args: Args) -> SourceResult<Styles> {
- let styles = (self.0.set)(&mut args)?;
- args.finish()?;
- Ok(styles)
- }
-}
-
-impl Debug for ElemFunc {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad(self.name())
- }
-}
-
-impl Eq for ElemFunc {}
-
-impl PartialEq for ElemFunc {
- fn eq(&self, other: &Self) -> bool {
- std::ptr::eq(self.0, other.0)
- }
-}
-
-impl Hash for ElemFunc {
- fn hash<H: Hasher>(&self, state: &mut H) {
- state.write_usize(self.0 as *const _ as usize);
- }
-}
-
-cast! {
- ElemFunc,
- self => Value::Func(self.into()),
- v: Func => v.element().ok_or("expected element function")?,
-}
-
-impl From<&'static NativeElemFunc> for ElemFunc {
- fn from(native: &'static NativeElemFunc) -> Self {
- Self(native)
- }
-}
-
-/// An element function backed by a Rust type.
-pub struct NativeElemFunc {
- /// The element's name.
- pub name: &'static str,
- /// The element's vtable for capability dispatch.
- pub vtable: fn(of: TypeId) -> Option<*const ()>,
- /// The element's constructor.
- pub construct: fn(&mut Vm, &mut Args) -> SourceResult<Content>,
- /// The element's set rule.
- pub set: fn(&mut Args) -> SourceResult<Styles>,
- /// Details about the function.
- pub info: Lazy<FuncInfo>,
-}
diff --git a/src/model/introspect.rs b/src/model/introspect.rs
deleted file mode 100644
index 42c1a9e1..00000000
--- a/src/model/introspect.rs
+++ /dev/null
@@ -1,352 +0,0 @@
-use std::cell::RefCell;
-use std::collections::HashMap;
-use std::fmt::{self, Debug, Formatter};
-use std::hash::Hash;
-use std::num::NonZeroUsize;
-
-use comemo::{Prehashed, Track, Tracked, Validate};
-use ecow::EcoVec;
-use indexmap::IndexMap;
-
-use super::{Content, Selector};
-use crate::diag::{bail, StrResult};
-use crate::doc::{Frame, FrameItem, Meta, Position};
-use crate::eval::{cast, Value};
-use crate::geom::{Point, Transform};
-use crate::model::Label;
-use crate::util::NonZeroExt;
-
-/// Identifies the location of an element in the document.
-///
-/// This struct is created by [`Locator::locate`].
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Location {
- /// The hash of the element.
- hash: u128,
- /// An unique number among elements with the same hash. This is the reason
- /// we need a `Locator` everywhere.
- disambiguator: usize,
- /// A synthetic location created from another one. This is used for example
- /// in bibliography management to create individual linkable locations for
- /// reference entries from the bibliography's location.
- variant: usize,
-}
-
-impl Location {
- /// Produce a variant of this location.
- pub fn variant(mut self, n: usize) -> Self {
- self.variant = n;
- self
- }
-}
-
-impl Debug for Location {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad("..")
- }
-}
-
-cast! {
- type Location: "location",
-}
-
-/// Provides locations for elements in the document.
-///
-/// A [`Location`] consists of an element's hash plus a disambiguator. Just the
-/// hash is not enough because we can have multiple equal elements with the same
-/// hash (not a hash collision, just equal elements!). Between these, we
-/// disambiguate with an increasing number. In principle, the disambiguator
-/// could just be counted up. However, counting is an impure operation and as
-/// such we can't count across a memoization boundary. [^1]
-///
-/// Instead, we only mutate within a single "layout run" and combine the results
-/// with disambiguators from an outer tracked locator. Thus, the locators form a
-/// "tracked chain". When a layout run ends, its mutations are discarded and, on
-/// the other side of the memoization boundary, we
-/// [reconstruct](Self::visit_frame) them from the resulting [frames](Frame).
-///
-/// [^1]: Well, we could with [`TrackedMut`](comemo::TrackedMut), but the
-/// overhead is quite high, especially since we need to save & undo the counting
-/// when only measuring.
-#[derive(Default)]
-pub struct Locator<'a> {
- /// Maps from a hash to the maximum number we've seen for this hash. This
- /// number becomes the `disambiguator`.
- hashes: RefCell<HashMap<u128, usize>>,
- /// An outer `Locator`, from which we can get disambiguator for hashes
- /// outside of the current "layout run".
- ///
- /// 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<Tracked<'a, Self, <Locator<'static> as Validate>::Constraint>>,
-}
-
-impl<'a> Locator<'a> {
- /// Create a new locator.
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Create a new chained locator.
- pub fn chained(outer: Tracked<'a, Self>) -> Self {
- Self { outer: Some(outer), ..Default::default() }
- }
-
- /// 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.hashes.borrow().is_empty() => outer,
- _ => Track::track(self),
- }
- }
-
- /// Produce a stable identifier for this call site.
- pub fn locate(&mut self, hash: u128) -> Location {
- // Get the current disambiguator for this hash.
- let disambiguator = self.disambiguator_impl(hash);
-
- // Bump the next disambiguator up by one.
- self.hashes.borrow_mut().insert(hash, disambiguator + 1);
-
- // Create the location in its default variant.
- Location { hash, disambiguator, variant: 0 }
- }
-
- /// Advance past a frame.
- pub fn visit_frame(&mut self, frame: &Frame) {
- for (_, item) in frame.items() {
- match item {
- FrameItem::Group(group) => self.visit_frame(&group.frame),
- FrameItem::Meta(Meta::Elem(elem), _) => {
- let mut hashes = self.hashes.borrow_mut();
- let loc = elem.location().unwrap();
- let entry = hashes.entry(loc.hash).or_default();
-
- // Next disambiguator needs to be at least one larger than
- // the maximum we've seen so far.
- *entry = (*entry).max(loc.disambiguator + 1);
- }
- _ => {}
- }
- }
- }
-
- /// Advance past a number of frames.
- pub fn visit_frames<'b>(&mut self, frames: impl IntoIterator<Item = &'b Frame>) {
- for frame in frames {
- self.visit_frame(frame);
- }
- }
-
- /// The current disambiguator for the given hash.
- fn disambiguator_impl(&self, hash: u128) -> usize {
- *self
- .hashes
- .borrow_mut()
- .entry(hash)
- .or_insert_with(|| self.outer.map_or(0, |outer| outer.disambiguator(hash)))
- }
-}
-
-#[comemo::track]
-impl<'a> Locator<'a> {
- /// The current disambiguator for the hash.
- fn disambiguator(&self, hash: u128) -> usize {
- self.disambiguator_impl(hash)
- }
-}
-
-/// Can be queried for elements and their positions.
-pub struct Introspector {
- /// The number of pages in the document.
- pages: usize,
- /// All introspectable elements.
- elems: IndexMap<Location, (Prehashed<Content>, Position)>,
- /// The page numberings, indexed by page number minus 1.
- page_numberings: Vec<Value>,
- /// Caches queries done on the introspector. This is important because
- /// even if all top-level queries are distinct, they often have shared
- /// subqueries. Example: Individual counter queries with `before` that
- /// all depend on a global counter query.
- queries: RefCell<HashMap<u128, EcoVec<Prehashed<Content>>>>,
-}
-
-impl Introspector {
- /// Create a new introspector.
- #[tracing::instrument(skip(frames))]
- pub fn new(frames: &[Frame]) -> Self {
- let mut introspector = Self {
- pages: frames.len(),
- elems: IndexMap::new(),
- page_numberings: vec![],
- queries: RefCell::default(),
- };
- for (i, frame) in frames.iter().enumerate() {
- let page = NonZeroUsize::new(1 + i).unwrap();
- introspector.extract(frame, page, Transform::identity());
- }
- introspector
- }
-
- /// Extract metadata from a frame.
- #[tracing::instrument(skip_all)]
- fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) {
- for (pos, item) in frame.items() {
- match item {
- FrameItem::Group(group) => {
- let ts = ts
- .pre_concat(Transform::translate(pos.x, pos.y))
- .pre_concat(group.transform);
- self.extract(&group.frame, page, ts);
- }
- FrameItem::Meta(Meta::Elem(content), _)
- if !self.elems.contains_key(&content.location().unwrap()) =>
- {
- let pos = pos.transform(ts);
- let ret = self.elems.insert(
- content.location().unwrap(),
- (Prehashed::new(content.clone()), Position { page, point: pos }),
- );
- assert!(ret.is_none(), "duplicate locations");
- }
- FrameItem::Meta(Meta::PageNumbering(numbering), _) => {
- self.page_numberings.push(numbering.clone());
- }
- _ => {}
- }
- }
- }
-
- /// Iterate over all locatable elements.
- pub fn all(&self) -> impl Iterator<Item = &Prehashed<Content>> + '_ {
- self.elems.values().map(|(c, _)| c)
- }
-
- /// Get an element by its location.
- fn get(&self, location: &Location) -> Option<&Prehashed<Content>> {
- self.elems.get(location).map(|(elem, _)| elem)
- }
-
- /// Get the index of this element among all.
- fn index(&self, elem: &Content) -> usize {
- self.elems
- .get_index_of(&elem.location().unwrap())
- .unwrap_or(usize::MAX)
- }
-}
-
-#[comemo::track]
-impl Introspector {
- /// Query for all matching elements.
- pub fn query(&self, selector: &Selector) -> EcoVec<Prehashed<Content>> {
- let hash = crate::util::hash128(selector);
- if let Some(output) = self.queries.borrow().get(&hash) {
- return output.clone();
- }
-
- let output = match selector {
- Selector::Elem(..)
- | Selector::Label(_)
- | Selector::Regex(_)
- | Selector::Can(_)
- | Selector::Or(_)
- | Selector::And(_) => {
- self.all().filter(|elem| selector.matches(elem)).cloned().collect()
- }
-
- Selector::Location(location) => {
- self.get(location).cloned().into_iter().collect()
- }
- Selector::Before { selector, end, inclusive } => {
- let mut list = self.query(selector);
- if let Some(end) = self.query_first(end) {
- // Determine which elements are before `end`.
- let split = match list
- .binary_search_by_key(&self.index(&end), |elem| self.index(elem))
- {
- // Element itself is contained.
- Ok(i) => i + *inclusive as usize,
- // Element itself is not contained.
- Err(i) => i,
- };
- list = list[..split].into();
- }
- list
- }
- Selector::After { selector, start, inclusive } => {
- let mut list = self.query(selector);
- if let Some(start) = self.query_first(start) {
- // Determine which elements are after `start`.
- let split = match list
- .binary_search_by_key(&self.index(&start), |elem| {
- self.index(elem)
- }) {
- // Element itself is contained.
- Ok(i) => i + !*inclusive as usize,
- // Element itself is not contained.
- Err(i) => i,
- };
- list = list[split..].into();
- }
- list
- }
- };
-
- self.queries.borrow_mut().insert(hash, output.clone());
- output
- }
-
- /// Query for the first element that matches the selector.
- pub fn query_first(&self, selector: &Selector) -> Option<Prehashed<Content>> {
- match selector {
- Selector::Location(location) => self.get(location).cloned(),
- _ => self.query(selector).first().cloned(),
- }
- }
-
- /// Query for a unique element with the label.
- pub fn query_label(&self, label: &Label) -> StrResult<Prehashed<Content>> {
- let mut found = None;
- for elem in self.all().filter(|elem| elem.label() == Some(label)) {
- if found.is_some() {
- bail!("label occurs multiple times in the document");
- }
- found = Some(elem.clone());
- }
- found.ok_or_else(|| "label does not exist in the document".into())
- }
-
- /// The total number pages.
- pub fn pages(&self) -> NonZeroUsize {
- NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE)
- }
-
- /// Gets the page numbering for the given location, if any.
- pub fn page_numbering(&self, location: Location) -> Value {
- let page = self.page(location);
- self.page_numberings.get(page.get() - 1).cloned().unwrap_or_default()
- }
-
- /// Find the page number for the given location.
- pub fn page(&self, location: Location) -> NonZeroUsize {
- self.position(location).page
- }
-
- /// Find the position for the given location.
- pub fn position(&self, location: Location) -> Position {
- self.elems
- .get(&location)
- .map(|(_, loc)| *loc)
- .unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() })
- }
-}
-
-impl Default for Introspector {
- fn default() -> Self {
- Self::new(&[])
- }
-}
diff --git a/src/model/label.rs b/src/model/label.rs
deleted file mode 100644
index ef8f3edd..00000000
--- a/src/model/label.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-use std::fmt::{self, Debug, Formatter};
-
-use ecow::EcoString;
-
-/// A label for an element.
-#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub struct Label(pub EcoString);
-
-impl Debug for Label {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "<{}>", self.0)
- }
-}
-
-/// Indicates that an element cannot be labelled.
-pub trait Unlabellable {}
diff --git a/src/model/mod.rs b/src/model/mod.rs
deleted file mode 100644
index ee940236..00000000
--- a/src/model/mod.rs
+++ /dev/null
@@ -1,148 +0,0 @@
-//! The document model.
-
-mod content;
-mod element;
-mod introspect;
-mod label;
-mod realize;
-mod selector;
-mod styles;
-
-#[doc(inline)]
-pub use typst_macros::element;
-
-pub use self::content::{Content, MetaElem, PlainText};
-pub use self::element::{Construct, ElemFunc, Element, NativeElemFunc, Set};
-pub use self::introspect::{Introspector, Location, Locator};
-pub use self::label::{Label, Unlabellable};
-pub use self::realize::{
- applicable, realize, Behave, Behaviour, Finalize, Guard, Locatable, Show, Synthesize,
-};
-pub use self::selector::{LocatableSelector, Selector, ShowableSelector};
-pub use self::styles::{
- Fold, Property, Recipe, Resolve, Style, StyleChain, StyleVec, StyleVecBuilder,
- Styles, Transform,
-};
-
-use std::mem::ManuallyDrop;
-
-use comemo::{Track, Tracked, TrackedMut, Validate};
-
-use crate::diag::{SourceError, SourceResult};
-use crate::doc::Document;
-use crate::eval::Tracer;
-use crate::World;
-
-/// Typeset content into a fully layouted document.
-#[comemo::memoize]
-#[tracing::instrument(skip(world, tracer, content))]
-pub fn typeset(
- world: Tracked<dyn World + '_>,
- mut tracer: TrackedMut<Tracer>,
- content: &Content,
-) -> SourceResult<Document> {
- tracing::info!("Starting typesetting");
-
- let library = world.library();
- let styles = StyleChain::new(&library.styles);
-
- let mut iter = 0;
- let mut document;
- let mut delayed;
-
- // We need `ManuallyDrop` until this lands in stable:
- // https://github.com/rust-lang/rust/issues/70919
- let mut introspector = ManuallyDrop::new(Introspector::new(&[]));
-
- // Relayout until all introspections stabilize.
- // If that doesn't happen within five attempts, we give up.
- loop {
- tracing::info!("Layout iteration {iter}");
-
- delayed = DelayedErrors::default();
-
- let constraint = <Introspector as Validate>::Constraint::new();
- let mut locator = Locator::new();
- let mut vt = Vt {
- world,
- tracer: TrackedMut::reborrow_mut(&mut tracer),
- locator: &mut locator,
- introspector: introspector.track_with(&constraint),
- delayed: delayed.track_mut(),
- };
-
- // Layout!
- let result = (library.items.layout)(&mut vt, content, styles)?;
-
- // Drop the old introspector.
- ManuallyDrop::into_inner(introspector);
-
- // Only now assign the document and construct the new introspector.
- document = result;
- introspector = ManuallyDrop::new(Introspector::new(&document.pages));
- iter += 1;
-
- if iter >= 5 || introspector.validate(&constraint) {
- break;
- }
- }
-
- // Drop the introspector.
- ManuallyDrop::into_inner(introspector);
-
- // Promote delayed errors.
- if !delayed.0.is_empty() {
- return Err(Box::new(delayed.0));
- }
-
- Ok(document)
-}
-
-/// A virtual typesetter.
-///
-/// Holds the state needed to [typeset] content.
-pub struct Vt<'a> {
- /// The compilation environment.
- pub world: Tracked<'a, dyn World + 'a>,
- /// Provides access to information about the document.
- pub introspector: Tracked<'a, Introspector>,
- /// Provides stable identities to elements.
- pub locator: &'a mut Locator<'a>,
- /// Delayed errors that do not immediately terminate execution.
- pub delayed: TrackedMut<'a, DelayedErrors>,
- /// The tracer for inspection of the values an expression produces.
- pub tracer: TrackedMut<'a, Tracer>,
-}
-
-impl Vt<'_> {
- /// Perform a fallible operation that does not immediately terminate further
- /// execution. Instead it produces a delayed error that is only promoted to
- /// a fatal one if it remains at the end of the introspection loop.
- pub fn delayed<F, T>(&mut self, f: F) -> T
- where
- F: FnOnce(&mut Self) -> SourceResult<T>,
- T: Default,
- {
- match f(self) {
- Ok(value) => value,
- Err(errors) => {
- for error in *errors {
- self.delayed.push(error);
- }
- T::default()
- }
- }
- }
-}
-
-/// Holds delayed errors.
-#[derive(Default, Clone)]
-pub struct DelayedErrors(Vec<SourceError>);
-
-#[comemo::track]
-impl DelayedErrors {
- /// Push a delayed error.
- fn push(&mut self, error: SourceError) {
- self.0.push(error);
- }
-}
diff --git a/src/model/realize.rs b/src/model/realize.rs
deleted file mode 100644
index 01c46b81..00000000
--- a/src/model/realize.rs
+++ /dev/null
@@ -1,228 +0,0 @@
-use super::{Content, ElemFunc, Element, MetaElem, Recipe, Selector, StyleChain, Vt};
-use crate::diag::SourceResult;
-use crate::doc::Meta;
-use crate::util::hash128;
-
-/// Whether the target is affected by show rules in the given style chain.
-pub fn applicable(target: &Content, styles: StyleChain) -> bool {
- if target.needs_preparation() {
- return true;
- }
-
- if target.can::<dyn Show>() && target.is_pristine() {
- return true;
- }
-
- // Find out how many recipes there are.
- let mut n = styles.recipes().count();
-
- // Find out whether any recipe matches and is unguarded.
- for recipe in styles.recipes() {
- if recipe.applicable(target) && !target.is_guarded(Guard::Nth(n)) {
- return true;
- }
- n -= 1;
- }
-
- false
-}
-
-/// Apply the show rules in the given style chain to a target.
-pub fn realize(
- vt: &mut Vt,
- target: &Content,
- styles: StyleChain,
-) -> SourceResult<Option<Content>> {
- // Pre-process.
- if target.needs_preparation() {
- let mut elem = target.clone();
- if target.can::<dyn Locatable>() || target.label().is_some() {
- let location = vt.locator.locate(hash128(target));
- elem.set_location(location);
- }
-
- if let Some(elem) = elem.with_mut::<dyn Synthesize>() {
- elem.synthesize(vt, styles)?;
- }
-
- elem.mark_prepared();
-
- if elem.location().is_some() {
- let span = elem.span();
- let meta = Meta::Elem(elem.clone());
- return Ok(Some(
- (elem + MetaElem::new().pack().spanned(span))
- .styled(MetaElem::set_data(vec![meta])),
- ));
- }
-
- return Ok(Some(elem));
- }
-
- // Find out how many recipes there are.
- let mut n = styles.recipes().count();
-
- // Find an applicable recipe.
- let mut realized = None;
- for recipe in styles.recipes() {
- let guard = Guard::Nth(n);
- if recipe.applicable(target) && !target.is_guarded(guard) {
- if let Some(content) = try_apply(vt, target, recipe, guard)? {
- realized = Some(content);
- break;
- }
- }
- n -= 1;
- }
-
- // Realize if there was no matching recipe.
- if let Some(showable) = target.with::<dyn Show>() {
- let guard = Guard::Base(target.func());
- if realized.is_none() && !target.is_guarded(guard) {
- realized = Some(showable.show(vt, styles)?);
- }
- }
-
- // Finalize only if this is the first application for this element.
- if let Some(elem) = target.with::<dyn Finalize>() {
- if target.is_pristine() {
- if let Some(already) = realized {
- realized = Some(elem.finalize(already, styles));
- }
- }
- }
-
- Ok(realized)
-}
-
-/// Try to apply a recipe to the target.
-fn try_apply(
- vt: &mut Vt,
- target: &Content,
- recipe: &Recipe,
- guard: Guard,
-) -> SourceResult<Option<Content>> {
- match &recipe.selector {
- Some(Selector::Elem(element, _)) => {
- if target.func() != *element {
- return Ok(None);
- }
-
- recipe.apply_vt(vt, target.clone().guarded(guard)).map(Some)
- }
-
- Some(Selector::Label(label)) => {
- if target.label() != Some(label) {
- return Ok(None);
- }
-
- recipe.apply_vt(vt, target.clone().guarded(guard)).map(Some)
- }
-
- Some(Selector::Regex(regex)) => {
- let Some(text) = item!(text_str)(target) else {
- return Ok(None);
- };
-
- let make = |s: &str| target.clone().with_field("text", s);
- let mut result = vec![];
- let mut cursor = 0;
-
- for m in regex.find_iter(&text) {
- let start = m.start();
- if cursor < start {
- result.push(make(&text[cursor..start]));
- }
-
- let piece = make(m.as_str()).guarded(guard);
- let transformed = recipe.apply_vt(vt, piece)?;
- result.push(transformed);
- cursor = m.end();
- }
-
- if result.is_empty() {
- return Ok(None);
- }
-
- if cursor < text.len() {
- result.push(make(&text[cursor..]));
- }
-
- Ok(Some(Content::sequence(result)))
- }
-
- // Not supported here.
- Some(
- Selector::Or(_)
- | Selector::And(_)
- | Selector::Location(_)
- | Selector::Can(_)
- | Selector::Before { .. }
- | Selector::After { .. },
- ) => Ok(None),
-
- None => Ok(None),
- }
-}
-
-/// Makes this element locatable through `vt.locate`.
-pub trait Locatable {}
-
-/// Synthesize fields on an element. This happens before execution of any show
-/// rule.
-pub trait Synthesize {
- /// Prepare the element for show rule application.
- fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()>;
-}
-
-/// The base recipe for an element.
-pub trait Show {
- /// Execute the base recipe for this element.
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>;
-}
-
-/// Post-process an element after it was realized.
-pub trait Finalize {
- /// Finalize the fully realized form of the element. Use this for effects
- /// that should work even in the face of a user-defined show rule.
- fn finalize(&self, realized: Content, styles: StyleChain) -> Content;
-}
-
-/// How the element interacts with other elements.
-pub trait Behave {
- /// The element's interaction behaviour.
- fn behaviour(&self) -> Behaviour;
-
- /// Whether this weak element is larger than a previous one and thus picked
- /// as the maximum when the levels are the same.
- #[allow(unused_variables)]
- fn larger(&self, prev: &Content) -> bool {
- false
- }
-}
-
-/// How an element interacts with other elements in a stream.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum Behaviour {
- /// A weak element which only survives when a supportive element is before
- /// and after it. Furthermore, per consecutive run of weak elements, only
- /// one survives: The one with the lowest weakness level (or the larger one
- /// if there is a tie).
- Weak(usize),
- /// An element that enables adjacent weak elements to exist. The default.
- Supportive,
- /// An element that destroys adjacent weak elements.
- Destructive,
- /// An element that does not interact at all with other elements, having the
- /// same effect as if it didn't exist.
- Ignorant,
-}
-
-/// Guards content against being affected by the same show rule multiple times.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Guard {
- /// The nth recipe from the top of the chain.
- Nth(usize),
- /// The [base recipe](Show) for a kind of element.
- Base(ElemFunc),
-}
diff --git a/src/model/selector.rs b/src/model/selector.rs
deleted file mode 100644
index 9723ee4f..00000000
--- a/src/model/selector.rs
+++ /dev/null
@@ -1,296 +0,0 @@
-use std::any::{Any, TypeId};
-use std::fmt::{self, Debug, Formatter, Write};
-use std::sync::Arc;
-
-use ecow::{eco_format, EcoString, EcoVec};
-
-use super::{Content, ElemFunc, Label, Location};
-use crate::diag::{bail, StrResult};
-use crate::eval::{
- cast, CastInfo, Dict, FromValue, Func, IntoValue, Reflect, Regex, Value,
-};
-use crate::model::Locatable;
-use crate::util::pretty_array_like;
-
-/// A selector in a show rule.
-#[derive(Clone, PartialEq, Hash)]
-pub enum Selector {
- /// Matches a specific type of element.
- ///
- /// If there is a dictionary, only elements with the fields from the
- /// dictionary match.
- Elem(ElemFunc, Option<Dict>),
- /// Matches the element at the specified location.
- Location(Location),
- /// Matches elements with a specific label.
- Label(Label),
- /// Matches text elements through a regular expression.
- Regex(Regex),
- /// Matches elements with a specific capability.
- Can(TypeId),
- /// Matches if any of the subselectors match.
- Or(EcoVec<Self>),
- /// Matches if all of the subselectors match.
- And(EcoVec<Self>),
- /// Matches all matches of `selector` before `end`.
- Before { selector: Arc<Self>, end: Arc<Self>, inclusive: bool },
- /// Matches all matches of `selector` after `start`.
- After { selector: Arc<Self>, start: Arc<Self>, inclusive: bool },
-}
-
-impl Selector {
- /// Define a simple text selector.
- pub fn text(text: &str) -> Self {
- Self::Regex(Regex::new(&regex::escape(text)).unwrap())
- }
-
- /// Define a simple [`Selector::Can`] selector.
- pub fn can<T: ?Sized + Any>() -> Self {
- Self::Can(TypeId::of::<T>())
- }
-
- /// Transforms this selector and an iterator of other selectors into a
- /// [`Selector::Or`] selector.
- pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self {
- Self::And(others.into_iter().chain(Some(self)).collect())
- }
-
- /// Transforms this selector and an iterator of other selectors into a
- /// [`Selector::And`] selector.
- pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self {
- Self::Or(others.into_iter().chain(Some(self)).collect())
- }
-
- /// Transforms this selector into a [`Selector::Before`] selector.
- pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self {
- Self::Before {
- selector: Arc::new(self),
- end: Arc::new(location.into()),
- inclusive,
- }
- }
-
- /// Transforms this selector into a [`Selector::After`] selector.
- pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self {
- Self::After {
- selector: Arc::new(self),
- start: Arc::new(location.into()),
- inclusive,
- }
- }
-
- /// Whether the selector matches for the target.
- pub fn matches(&self, target: &Content) -> bool {
- match self {
- Self::Elem(element, dict) => {
- target.func() == *element
- && dict
- .iter()
- .flat_map(|dict| dict.iter())
- .all(|(name, value)| target.field_ref(name) == Some(value))
- }
- Self::Label(label) => target.label() == Some(label),
- Self::Regex(regex) => {
- target.func() == item!(text_func)
- && item!(text_str)(target).map_or(false, |text| regex.is_match(&text))
- }
- Self::Can(cap) => target.can_type_id(*cap),
- Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)),
- Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)),
- Self::Location(location) => target.location() == Some(*location),
- // Not supported here.
- Self::Before { .. } | Self::After { .. } => false,
- }
- }
-}
-
-impl From<Location> for Selector {
- fn from(value: Location) -> Self {
- Self::Location(value)
- }
-}
-
-impl Debug for Selector {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Elem(elem, dict) => {
- f.write_str(elem.name())?;
- if let Some(dict) = dict {
- f.write_str(".where")?;
- dict.fmt(f)?;
- }
- Ok(())
- }
- Self::Label(label) => label.fmt(f),
- Self::Regex(regex) => regex.fmt(f),
- Self::Can(cap) => cap.fmt(f),
- Self::Or(selectors) | Self::And(selectors) => {
- f.write_str(if matches!(self, Self::Or(_)) { "or" } else { "and" })?;
- let pieces: Vec<_> =
- selectors.iter().map(|sel| eco_format!("{sel:?}")).collect();
- f.write_str(&pretty_array_like(&pieces, false))
- }
- Self::Location(loc) => loc.fmt(f),
- Self::Before { selector, end: split, inclusive }
- | Self::After { selector, start: split, inclusive } => {
- selector.fmt(f)?;
-
- if matches!(self, Self::Before { .. }) {
- f.write_str(".before(")?;
- } else {
- f.write_str(".after(")?;
- }
-
- split.fmt(f)?;
- if !*inclusive {
- f.write_str(", inclusive: false")?;
- }
- f.write_char(')')
- }
- }
- }
-}
-
-cast! {
- type Selector: "selector",
- func: Func => func
- .element()
- .ok_or("only element functions can be used as selectors")?
- .select(),
- label: Label => Self::Label(label),
- text: EcoString => Self::text(&text),
- regex: Regex => Self::Regex(regex),
- location: Location => Self::Location(location),
-}
-
-/// A selector that can be used with `query`.
-///
-/// Hopefully, this is made obsolete by a more powerful query mechanism in the
-/// future.
-#[derive(Clone, PartialEq, Hash)]
-pub struct LocatableSelector(pub Selector);
-
-impl Reflect for LocatableSelector {
- fn describe() -> CastInfo {
- CastInfo::Union(vec![
- CastInfo::Type("function"),
- CastInfo::Type("label"),
- CastInfo::Type("selector"),
- ])
- }
-
- fn castable(value: &Value) -> bool {
- matches!(value.type_name(), "function" | "label" | "selector")
- }
-}
-
-impl IntoValue for LocatableSelector {
- fn into_value(self) -> Value {
- self.0.into_value()
- }
-}
-
-impl FromValue for LocatableSelector {
- fn from_value(value: Value) -> StrResult<Self> {
- fn validate(selector: &Selector) -> StrResult<()> {
- match selector {
- Selector::Elem(elem, _) => {
- if !elem.can::<dyn Locatable>() {
- Err(eco_format!("{} is not locatable", elem.name()))?
- }
- }
- Selector::Location(_) => {}
- Selector::Label(_) => {}
- Selector::Regex(_) => bail!("text is not locatable"),
- Selector::Can(_) => bail!("capability is not locatable"),
- Selector::Or(list) | Selector::And(list) => {
- for selector in list {
- validate(selector)?;
- }
- }
- Selector::Before { selector, end: split, .. }
- | Selector::After { selector, start: split, .. } => {
- for selector in [selector, split] {
- validate(selector)?;
- }
- }
- }
- Ok(())
- }
-
- if !Self::castable(&value) {
- return Err(Self::error(&value));
- }
-
- let selector = Selector::from_value(value)?;
- validate(&selector)?;
- Ok(Self(selector))
- }
-}
-
-/// A selector that can be used with show rules.
-///
-/// Hopefully, this is made obsolete by a more powerful showing mechanism in the
-/// future.
-#[derive(Clone, PartialEq, Hash)]
-pub struct ShowableSelector(pub Selector);
-
-impl Reflect for ShowableSelector {
- fn describe() -> CastInfo {
- CastInfo::Union(vec![
- CastInfo::Type("function"),
- CastInfo::Type("label"),
- CastInfo::Type("string"),
- CastInfo::Type("regular expression"),
- CastInfo::Type("symbol"),
- CastInfo::Type("selector"),
- ])
- }
-
- fn castable(value: &Value) -> bool {
- matches!(
- value.type_name(),
- "symbol"
- | "string"
- | "label"
- | "function"
- | "regular expression"
- | "selector"
- )
- }
-}
-
-impl IntoValue for ShowableSelector {
- fn into_value(self) -> Value {
- self.0.into_value()
- }
-}
-
-impl FromValue for ShowableSelector {
- fn from_value(value: Value) -> StrResult<Self> {
- fn validate(selector: &Selector) -> StrResult<()> {
- match selector {
- Selector::Elem(_, _) => {}
- Selector::Label(_) => {}
- Selector::Regex(_) => {}
- Selector::Or(_)
- | Selector::And(_)
- | Selector::Location(_)
- | Selector::Can(_)
- | Selector::Before { .. }
- | Selector::After { .. } => {
- bail!("this selector cannot be used with show")
- }
- }
- Ok(())
- }
-
- if !Self::castable(&value) {
- return Err(Self::error(&value));
- }
-
- let selector = Selector::from_value(value)?;
- validate(&selector)?;
- Ok(Self(selector))
- }
-}
diff --git a/src/model/styles.rs b/src/model/styles.rs
deleted file mode 100644
index 23748a3f..00000000
--- a/src/model/styles.rs
+++ /dev/null
@@ -1,750 +0,0 @@
-use std::fmt::{self, Debug, Formatter, Write};
-use std::iter;
-use std::mem;
-use std::ptr;
-
-use comemo::Prehashed;
-use ecow::{eco_vec, EcoString, EcoVec};
-
-use super::{Content, ElemFunc, Element, Selector, Vt};
-use crate::diag::{SourceResult, Trace, Tracepoint};
-use crate::eval::{cast, Args, FromValue, Func, IntoValue, Value, Vm};
-use crate::syntax::Span;
-
-/// A list of style properties.
-#[derive(Default, PartialEq, Clone, Hash)]
-pub struct Styles(EcoVec<Prehashed<Style>>);
-
-impl Styles {
- /// Create a new, empty style list.
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Whether this contains no styles.
- pub fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-
- /// Set an inner value for a style property.
- ///
- /// If the property needs folding and the value is already contained in the
- /// style map, `self` contributes the outer values and `value` is the inner
- /// one.
- pub fn set(&mut self, style: impl Into<Style>) {
- self.0.push(Prehashed::new(style.into()));
- }
-
- /// Remove the style that was last set.
- pub fn unset(&mut self) {
- self.0.pop();
- }
-
- /// Apply outer styles. Like [`chain`](StyleChain::chain), but in-place.
- pub fn apply(&mut self, mut outer: Self) {
- outer.0.extend(mem::take(self).0.into_iter());
- *self = outer;
- }
-
- /// Apply one outer styles.
- pub fn apply_one(&mut self, outer: Style) {
- self.0.insert(0, Prehashed::new(outer));
- }
-
- /// Apply a slice of outer styles.
- pub fn apply_slice(&mut self, outer: &[Prehashed<Style>]) {
- self.0 = outer.iter().cloned().chain(mem::take(self).0.into_iter()).collect();
- }
-
- /// Add an origin span to all contained properties.
- pub fn spanned(mut self, span: Span) -> Self {
- for entry in self.0.make_mut() {
- entry.update(|entry| {
- if let Style::Property(property) = entry {
- property.span = Some(span);
- }
- });
- }
- self
- }
-
- /// Returns `Some(_)` with an optional span if this list contains
- /// styles for the given element.
- pub fn interruption<T: Element>(&self) -> Option<Option<Span>> {
- let func = T::func();
- self.0.iter().find_map(|entry| match &**entry {
- Style::Property(property) => property.is_of(func).then_some(property.span),
- Style::Recipe(recipe) => recipe.is_of(func).then_some(Some(recipe.span)),
- })
- }
-}
-
-impl From<Style> for Styles {
- fn from(entry: Style) -> Self {
- Self(eco_vec![Prehashed::new(entry)])
- }
-}
-
-impl Debug for Styles {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad("..")
- }
-}
-
-/// A single style property or recipe.
-#[derive(Clone, PartialEq, Hash)]
-pub enum Style {
- /// A style property originating from a set rule or constructor.
- Property(Property),
- /// A show rule recipe.
- Recipe(Recipe),
-}
-
-impl Style {
- /// If this is a property, return it.
- pub fn property(&self) -> Option<&Property> {
- match self {
- Self::Property(property) => Some(property),
- _ => None,
- }
- }
-
- /// If this is a recipe, return it.
- pub fn recipe(&self) -> Option<&Recipe> {
- match self {
- Self::Recipe(recipe) => Some(recipe),
- _ => None,
- }
- }
-}
-
-impl Debug for Style {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Property(property) => property.fmt(f),
- Self::Recipe(recipe) => recipe.fmt(f),
- }
- }
-}
-
-impl From<Property> for Style {
- fn from(property: Property) -> Self {
- Self::Property(property)
- }
-}
-
-impl From<Recipe> for Style {
- fn from(recipe: Recipe) -> Self {
- Self::Recipe(recipe)
- }
-}
-
-/// A style property originating from a set rule or constructor.
-#[derive(Clone, PartialEq, Hash)]
-pub struct Property {
- /// The element the property belongs to.
- element: ElemFunc,
- /// The property's name.
- name: EcoString,
- /// The property's value.
- value: Value,
- /// The span of the set rule the property stems from.
- span: Option<Span>,
-}
-
-impl Property {
- /// Create a new property from a key-value pair.
- pub fn new(
- element: ElemFunc,
- name: impl Into<EcoString>,
- value: impl IntoValue,
- ) -> Self {
- Self {
- element,
- name: name.into(),
- value: value.into_value(),
- span: None,
- }
- }
-
- /// Whether this property is the given one.
- pub fn is(&self, element: ElemFunc, name: &str) -> bool {
- self.element == element && self.name == name
- }
-
- /// Whether this property belongs to the given element.
- pub fn is_of(&self, element: ElemFunc) -> bool {
- self.element == element
- }
-}
-
-impl Debug for Property {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "set {}({}: {:?})", self.element.name(), self.name, self.value)?;
- Ok(())
- }
-}
-
-/// A show rule recipe.
-#[derive(Clone, PartialEq, Hash)]
-pub struct Recipe {
- /// The span errors are reported with.
- pub span: Span,
- /// Determines whether the recipe applies to an element.
- pub selector: Option<Selector>,
- /// The transformation to perform on the match.
- pub transform: Transform,
-}
-
-impl Recipe {
- /// Whether this recipe is for the given type of element.
- pub fn is_of(&self, element: ElemFunc) -> bool {
- match self.selector {
- Some(Selector::Elem(own, _)) => own == element,
- _ => false,
- }
- }
-
- /// Whether the recipe is applicable to the target.
- pub fn applicable(&self, target: &Content) -> bool {
- self.selector
- .as_ref()
- .map_or(false, |selector| selector.matches(target))
- }
-
- /// Apply the recipe to the given content.
- pub fn apply_vm(&self, vm: &mut Vm, content: Content) -> SourceResult<Content> {
- match &self.transform {
- Transform::Content(content) => Ok(content.clone()),
- Transform::Func(func) => {
- let args = Args::new(self.span, [Value::Content(content.clone())]);
- let mut result = func.call_vm(vm, args);
- // For selector-less show rules, a tracepoint makes no sense.
- if self.selector.is_some() {
- let point = || Tracepoint::Show(content.func().name().into());
- result = result.trace(vm.world(), point, content.span());
- }
- Ok(result?.display())
- }
- Transform::Style(styles) => Ok(content.styled_with_map(styles.clone())),
- }
- }
-
- /// Apply the recipe to the given content.
- pub fn apply_vt(&self, vt: &mut Vt, content: Content) -> SourceResult<Content> {
- match &self.transform {
- Transform::Content(content) => Ok(content.clone()),
- Transform::Func(func) => {
- let mut result = func.call_vt(vt, [Value::Content(content.clone())]);
- if self.selector.is_some() {
- let point = || Tracepoint::Show(content.func().name().into());
- result = result.trace(vt.world, point, content.span());
- }
- Ok(result?.display())
- }
- Transform::Style(styles) => Ok(content.styled_with_map(styles.clone())),
- }
- }
-}
-
-impl Debug for Recipe {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_str("show")?;
- if let Some(selector) = &self.selector {
- f.write_char(' ')?;
- selector.fmt(f)?;
- }
- f.write_str(": ")?;
- self.transform.fmt(f)
- }
-}
-
-/// A show rule transformation that can be applied to a match.
-#[derive(Clone, PartialEq, Hash)]
-pub enum Transform {
- /// Replacement content.
- Content(Content),
- /// A function to apply to the match.
- Func(Func),
- /// Apply styles to the content.
- Style(Styles),
-}
-
-impl Debug for Transform {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Content(content) => content.fmt(f),
- Self::Func(func) => func.fmt(f),
- Self::Style(styles) => styles.fmt(f),
- }
- }
-}
-
-cast! {
- Transform,
- content: Content => Self::Content(content),
- func: Func => Self::Func(func),
-}
-
-/// A chain of styles, similar to a linked list.
-///
-/// A style chain allows to combine properties from multiple style lists in a
-/// element hierarchy in a non-allocating way. Rather than eagerly merging the
-/// lists, each access walks the hierarchy from the innermost to the outermost
-/// map, trying to find a match and then folding it with matches further up the
-/// chain.
-#[derive(Default, Clone, Copy, Hash)]
-pub struct StyleChain<'a> {
- /// The first link of this chain.
- head: &'a [Prehashed<Style>],
- /// The remaining links in the chain.
- tail: Option<&'a Self>,
-}
-
-impl<'a> StyleChain<'a> {
- /// Start a new style chain with root styles.
- pub fn new(root: &'a Styles) -> Self {
- Self { head: &root.0, tail: None }
- }
-
- /// Make the given style list the first link of this chain.
- ///
- /// The resulting style chain contains styles from `local` as well as
- /// `self`. The ones from `local` take precedence over the ones from
- /// `self`. For folded properties `local` contributes the inner value.
- pub fn chain<'b>(&'b self, local: &'b Styles) -> StyleChain<'b> {
- if local.is_empty() {
- *self
- } else {
- StyleChain { head: &local.0, tail: Some(self) }
- }
- }
-
- /// Cast the first value for the given property in the chain.
- pub fn get<T: FromValue>(
- self,
- func: ElemFunc,
- name: &'a str,
- inherent: Option<Value>,
- default: impl Fn() -> T,
- ) -> T {
- self.properties::<T>(func, name, inherent)
- .next()
- .unwrap_or_else(default)
- }
-
- /// Cast the first value for the given property in the chain.
- pub fn get_resolve<T: FromValue + Resolve>(
- self,
- func: ElemFunc,
- name: &'a str,
- inherent: Option<Value>,
- default: impl Fn() -> T,
- ) -> T::Output {
- self.get(func, name, inherent, default).resolve(self)
- }
-
- /// Cast the first value for the given property in the chain.
- pub fn get_fold<T: FromValue + Fold>(
- self,
- func: ElemFunc,
- name: &'a str,
- inherent: Option<Value>,
- default: impl Fn() -> T::Output,
- ) -> T::Output {
- fn next<T: Fold>(
- mut values: impl Iterator<Item = T>,
- _styles: StyleChain,
- default: &impl Fn() -> T::Output,
- ) -> T::Output {
- values
- .next()
- .map(|value| value.fold(next(values, _styles, default)))
- .unwrap_or_else(default)
- }
- next(self.properties::<T>(func, name, inherent), self, &default)
- }
-
- /// Cast the first value for the given property in the chain.
- pub fn get_resolve_fold<T>(
- self,
- func: ElemFunc,
- name: &'a str,
- inherent: Option<Value>,
- default: impl Fn() -> <T::Output as Fold>::Output,
- ) -> <T::Output as Fold>::Output
- where
- T: FromValue + Resolve,
- T::Output: Fold,
- {
- fn next<T>(
- mut values: impl Iterator<Item = T>,
- styles: StyleChain,
- default: &impl Fn() -> <T::Output as Fold>::Output,
- ) -> <T::Output as Fold>::Output
- where
- T: Resolve,
- T::Output: Fold,
- {
- values
- .next()
- .map(|value| value.resolve(styles).fold(next(values, styles, default)))
- .unwrap_or_else(default)
- }
- next(self.properties::<T>(func, name, inherent), self, &default)
- }
-
- /// Iterate over all style recipes in the chain.
- pub fn recipes(self) -> impl Iterator<Item = &'a Recipe> {
- self.entries().filter_map(Style::recipe)
- }
-
- /// Iterate over all values for the given property in the chain.
- pub fn properties<T: FromValue + 'a>(
- self,
- func: ElemFunc,
- name: &'a str,
- inherent: Option<Value>,
- ) -> impl Iterator<Item = T> + '_ {
- inherent
- .into_iter()
- .chain(
- self.entries()
- .filter_map(Style::property)
- .filter(move |property| property.is(func, name))
- .map(|property| property.value.clone()),
- )
- .map(move |value| {
- value.cast().unwrap_or_else(|err| {
- panic!("{} (for {}.{})", err, func.name(), name)
- })
- })
- }
-
- /// Convert to a style map.
- pub fn to_map(self) -> Styles {
- let mut suffix = Styles::new();
- for link in self.links() {
- suffix.apply_slice(link);
- }
- suffix
- }
-
- /// Iterate over the entries of the chain.
- fn entries(self) -> Entries<'a> {
- Entries { inner: [].as_slice().iter(), links: self.links() }
- }
-
- /// Iterate over the links of the chain.
- fn links(self) -> Links<'a> {
- Links(Some(self))
- }
-
- /// Build owned styles from the suffix (all links beyond the `len`) of the
- /// chain.
- fn suffix(self, len: usize) -> Styles {
- let mut suffix = Styles::new();
- let take = self.links().count().saturating_sub(len);
- for link in self.links().take(take) {
- suffix.apply_slice(link);
- }
- suffix
- }
-
- /// Remove the last link from the chain.
- fn pop(&mut self) {
- *self = self.tail.copied().unwrap_or_default();
- }
-}
-
-impl Debug for StyleChain<'_> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- for entry in self.entries().collect::<Vec<_>>().into_iter().rev() {
- writeln!(f, "{:?}", entry)?;
- }
- Ok(())
- }
-}
-
-impl PartialEq for StyleChain<'_> {
- fn eq(&self, other: &Self) -> bool {
- ptr::eq(self.head, other.head)
- && match (self.tail, other.tail) {
- (Some(a), Some(b)) => ptr::eq(a, b),
- (None, None) => true,
- _ => false,
- }
- }
-}
-
-/// An iterator over the entries in a style chain.
-struct Entries<'a> {
- inner: std::slice::Iter<'a, Prehashed<Style>>,
- links: Links<'a>,
-}
-
-impl<'a> Iterator for Entries<'a> {
- type Item = &'a Style;
-
- fn next(&mut self) -> Option<Self::Item> {
- loop {
- if let Some(entry) = self.inner.next_back() {
- return Some(entry);
- }
-
- match self.links.next() {
- Some(next) => self.inner = next.iter(),
- None => return None,
- }
- }
- }
-}
-
-/// An iterator over the links of a style chain.
-struct Links<'a>(Option<StyleChain<'a>>);
-
-impl<'a> Iterator for Links<'a> {
- type Item = &'a [Prehashed<Style>];
-
- fn next(&mut self) -> Option<Self::Item> {
- let StyleChain { head, tail } = self.0?;
- self.0 = tail.copied();
- Some(head)
- }
-}
-
-/// A sequence of items with associated styles.
-#[derive(Clone, Hash)]
-pub struct StyleVec<T> {
- items: Vec<T>,
- styles: Vec<(Styles, usize)>,
-}
-
-impl<T> StyleVec<T> {
- /// Whether there are any items in the sequence.
- pub fn is_empty(&self) -> bool {
- self.items.is_empty()
- }
-
- /// Number of items in the sequence.
- pub fn len(&self) -> usize {
- self.items.len()
- }
-
- /// Insert an item in the front. The item will share the style of the
- /// current first item.
- ///
- /// This method has no effect if the vector is empty.
- pub fn push_front(&mut self, item: T) {
- if !self.styles.is_empty() {
- self.items.insert(0, item);
- self.styles[0].1 += 1;
- }
- }
-
- /// Map the contained items.
- pub fn map<F, U>(&self, f: F) -> StyleVec<U>
- where
- F: FnMut(&T) -> U,
- {
- StyleVec {
- items: self.items.iter().map(f).collect(),
- styles: self.styles.clone(),
- }
- }
-
- /// Iterate over references to the contained items and associated styles.
- pub fn iter(&self) -> impl Iterator<Item = (&T, &Styles)> + '_ {
- self.items().zip(
- self.styles
- .iter()
- .flat_map(|(map, count)| iter::repeat(map).take(*count)),
- )
- }
-
- /// Iterate over the contained items.
- pub fn items(&self) -> std::slice::Iter<'_, T> {
- self.items.iter()
- }
-
- /// Iterate over the contained style lists. Note that zipping this with
- /// `items()` does not yield the same result as calling `iter()` because
- /// this method only returns lists once that are shared by consecutive
- /// items. This method is designed for use cases where you want to check,
- /// for example, whether any of the lists fulfills a specific property.
- pub fn styles(&self) -> impl Iterator<Item = &Styles> {
- self.styles.iter().map(|(map, _)| map)
- }
-}
-
-impl StyleVec<Content> {
- pub fn to_vec(self) -> Vec<Content> {
- self.items
- .into_iter()
- .zip(
- self.styles
- .iter()
- .flat_map(|(map, count)| iter::repeat(map).take(*count)),
- )
- .map(|(content, styles)| content.styled_with_map(styles.clone()))
- .collect()
- }
-}
-
-impl<T> Default for StyleVec<T> {
- fn default() -> Self {
- Self { items: vec![], styles: vec![] }
- }
-}
-
-impl<T> FromIterator<T> for StyleVec<T> {
- fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
- let items: Vec<_> = iter.into_iter().collect();
- let styles = vec![(Styles::new(), items.len())];
- Self { items, styles }
- }
-}
-
-impl<T: Debug> Debug for StyleVec<T> {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.debug_list()
- .entries(self.iter().map(|(item, styles)| {
- crate::util::debug(|f| {
- styles.fmt(f)?;
- item.fmt(f)
- })
- }))
- .finish()
- }
-}
-
-/// Assists in the construction of a [`StyleVec`].
-#[derive(Debug)]
-pub struct StyleVecBuilder<'a, T> {
- items: Vec<T>,
- chains: Vec<(StyleChain<'a>, usize)>,
-}
-
-impl<'a, T> StyleVecBuilder<'a, T> {
- /// Create a new style-vec builder.
- pub fn new() -> Self {
- Self { items: vec![], chains: vec![] }
- }
-
- /// Whether the builder is empty.
- pub fn is_empty(&self) -> bool {
- self.items.is_empty()
- }
-
- /// Push a new item into the style vector.
- pub fn push(&mut self, item: T, styles: StyleChain<'a>) {
- self.items.push(item);
-
- if let Some((prev, count)) = self.chains.last_mut() {
- if *prev == styles {
- *count += 1;
- return;
- }
- }
-
- self.chains.push((styles, 1));
- }
-
- /// Iterate over the contained items.
- pub fn elems(&self) -> std::slice::Iter<'_, T> {
- self.items.iter()
- }
-
- /// Finish building, returning a pair of two things:
- /// - a style vector of items with the non-shared styles
- /// - a shared prefix chain of styles that apply to all items
- pub fn finish(self) -> (StyleVec<T>, StyleChain<'a>) {
- let mut iter = self.chains.iter();
- let mut trunk = match iter.next() {
- Some(&(chain, _)) => chain,
- None => return Default::default(),
- };
-
- let mut shared = trunk.links().count();
- for &(mut chain, _) in iter {
- let len = chain.links().count();
- if len < shared {
- for _ in 0..shared - len {
- trunk.pop();
- }
- shared = len;
- } else if len > shared {
- for _ in 0..len - shared {
- chain.pop();
- }
- }
-
- while shared > 0 && chain != trunk {
- trunk.pop();
- chain.pop();
- shared -= 1;
- }
- }
-
- let styles = self
- .chains
- .into_iter()
- .map(|(chain, count)| (chain.suffix(shared), count))
- .collect();
-
- (StyleVec { items: self.items, styles }, trunk)
- }
-}
-
-impl<'a, T> Default for StyleVecBuilder<'a, T> {
- fn default() -> Self {
- Self::new()
- }
-}
-
-/// A property that is resolved with other properties from the style chain.
-pub trait Resolve {
- /// The type of the resolved output.
- type Output;
-
- /// Resolve the value using the style chain.
- fn resolve(self, styles: StyleChain) -> Self::Output;
-}
-
-impl<T: Resolve> Resolve for Option<T> {
- type Output = Option<T::Output>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- self.map(|v| v.resolve(styles))
- }
-}
-
-/// A property that is folded to determine its final value.
-///
-/// In the example below, the chain of stroke values is folded into a single
-/// value: `4pt + red`.
-///
-/// ```example
-/// #set rect(stroke: red)
-/// #set rect(stroke: 4pt)
-/// #rect()
-/// ```
-pub trait Fold {
- /// The type of the folded output.
- type Output;
-
- /// Fold this inner value with an outer folded value.
- fn fold(self, outer: Self::Output) -> Self::Output;
-}
-
-impl<T> Fold for Option<T>
-where
- T: Fold,
- T::Output: Default,
-{
- type Output = Option<T::Output>;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.map(|inner| inner.fold(outer.unwrap_or_default()))
- }
-}
diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs
deleted file mode 100644
index 7d5e2989..00000000
--- a/src/syntax/ast.rs
+++ /dev/null
@@ -1,1994 +0,0 @@
-//! A typed layer over the untyped syntax tree.
-//!
-//! The AST is rooted in the [`Markup`] node.
-
-use std::num::NonZeroUsize;
-use std::ops::Deref;
-
-use ecow::EcoString;
-use unscanny::Scanner;
-
-use super::{
- is_id_continue, is_id_start, is_newline, split_newlines, Span, SyntaxKind, SyntaxNode,
-};
-use crate::geom::{AbsUnit, AngleUnit};
-use crate::util::NonZeroExt;
-
-/// A typed AST node.
-pub trait AstNode: Sized {
- /// Convert a node into its typed variant.
- fn from_untyped(node: &SyntaxNode) -> Option<Self>;
-
- /// A reference to the underlying syntax node.
- fn as_untyped(&self) -> &SyntaxNode;
-
- /// The source code location.
- fn span(&self) -> Span {
- self.as_untyped().span()
- }
-}
-
-macro_rules! node {
- ($(#[$attr:meta])* $name:ident) => {
- #[derive(Debug, Default, Clone, Hash)]
- #[repr(transparent)]
- $(#[$attr])*
- pub struct $name(SyntaxNode);
-
- impl AstNode for $name {
- fn from_untyped(node: &SyntaxNode) -> Option<Self> {
- if matches!(node.kind(), SyntaxKind::$name) {
- Some(Self(node.clone()))
- } else {
- Option::None
- }
- }
-
- fn as_untyped(&self) -> &SyntaxNode {
- &self.0
- }
- }
- };
-}
-
-node! {
- /// The syntactical root capable of representing a full parsed document.
- Markup
-}
-
-impl Markup {
- /// The expressions.
- pub fn exprs(&self) -> impl DoubleEndedIterator<Item = Expr> + '_ {
- let mut was_stmt = false;
- self.0
- .children()
- .filter(move |node| {
- // Ignore newline directly after statements without semicolons.
- let kind = node.kind();
- let keep = !was_stmt || node.kind() != SyntaxKind::Space;
- was_stmt = kind.is_stmt();
- keep
- })
- .filter_map(Expr::cast_with_space)
- }
-}
-
-/// An expression in markup, math or code.
-#[derive(Debug, Clone, Hash)]
-pub enum Expr {
- /// Plain text without markup.
- Text(Text),
- /// Whitespace in markup or math. Has at most one newline in markup, as more
- /// indicate a paragraph break.
- Space(Space),
- /// A forced line break: `\`.
- Linebreak(Linebreak),
- /// A paragraph break, indicated by one or multiple blank lines.
- Parbreak(Parbreak),
- /// An escape sequence: `\#`, `\u{1F5FA}`.
- Escape(Escape),
- /// A shorthand for a unicode codepoint. For example, `~` for non-breaking
- /// space or `-?` for a soft hyphen.
- Shorthand(Shorthand),
- /// A smart quote: `'` or `"`.
- SmartQuote(SmartQuote),
- /// Strong content: `*Strong*`.
- Strong(Strong),
- /// Emphasized content: `_Emphasized_`.
- Emph(Emph),
- /// Raw text with optional syntax highlighting: `` `...` ``.
- Raw(Raw),
- /// A hyperlink: `https://typst.org`.
- Link(Link),
- /// A label: `<intro>`.
- Label(Label),
- /// A reference: `@target`, `@target[..]`.
- Ref(Ref),
- /// A section heading: `= Introduction`.
- Heading(Heading),
- /// An item in a bullet list: `- ...`.
- List(ListItem),
- /// An item in an enumeration (numbered list): `+ ...` or `1. ...`.
- Enum(EnumItem),
- /// An item in a term list: `/ Term: Details`.
- Term(TermItem),
- /// A mathematical equation: `$x$`, `$ x^2 $`.
- Equation(Equation),
- /// The contents of a mathematical equation: `x^2 + 1`.
- Math(Math),
- /// An identifier in math: `pi`.
- MathIdent(MathIdent),
- /// An alignment point in math: `&`.
- MathAlignPoint(MathAlignPoint),
- /// Matched delimiters in math: `[x + y]`.
- MathDelimited(MathDelimited),
- /// A base with optional attachments in math: `a_1^2`.
- MathAttach(MathAttach),
- /// A fraction in math: `x/2`.
- MathFrac(MathFrac),
- /// A root in math: `√x`, `∛x` or `∜x`.
- MathRoot(MathRoot),
- /// An identifier: `left`.
- Ident(Ident),
- /// The `none` literal.
- None(None),
- /// The `auto` literal.
- Auto(Auto),
- /// A boolean: `true`, `false`.
- Bool(Bool),
- /// An integer: `120`.
- Int(Int),
- /// A floating-point number: `1.2`, `10e-4`.
- Float(Float),
- /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`.
- Numeric(Numeric),
- /// A quoted string: `"..."`.
- Str(Str),
- /// A code block: `{ let x = 1; x + 2 }`.
- Code(CodeBlock),
- /// A content block: `[*Hi* there!]`.
- Content(ContentBlock),
- /// A grouped expression: `(1 + 2)`.
- Parenthesized(Parenthesized),
- /// An array: `(1, "hi", 12cm)`.
- Array(Array),
- /// A dictionary: `(thickness: 3pt, pattern: dashed)`.
- Dict(Dict),
- /// A unary operation: `-x`.
- Unary(Unary),
- /// A binary operation: `a + b`.
- Binary(Binary),
- /// A field access: `properties.age`.
- FieldAccess(FieldAccess),
- /// An invocation of a function or method: `f(x, y)`.
- FuncCall(FuncCall),
- /// A closure: `(x, y) => z`.
- Closure(Closure),
- /// A let binding: `let x = 1`.
- Let(LetBinding),
- //// A destructuring assignment: `(x, y) = (1, 2)`.
- DestructAssign(DestructAssignment),
- /// A set rule: `set text(...)`.
- Set(SetRule),
- /// A show rule: `show heading: it => emph(it.body)`.
- Show(ShowRule),
- /// An if-else conditional: `if x { y } else { z }`.
- Conditional(Conditional),
- /// A while loop: `while x { y }`.
- While(WhileLoop),
- /// A for loop: `for x in y { z }`.
- For(ForLoop),
- /// A module import: `import "utils.typ": a, b, c`.
- Import(ModuleImport),
- /// A module include: `include "chapter1.typ"`.
- Include(ModuleInclude),
- /// A break from a loop: `break`.
- Break(LoopBreak),
- /// A continue in a loop: `continue`.
- Continue(LoopContinue),
- /// A return from a function: `return`, `return x + 1`.
- Return(FuncReturn),
-}
-
-impl Expr {
- fn cast_with_space(node: &SyntaxNode) -> Option<Self> {
- match node.kind() {
- SyntaxKind::Space => node.cast().map(Self::Space),
- _ => Self::from_untyped(node),
- }
- }
-}
-
-impl AstNode for Expr {
- fn from_untyped(node: &SyntaxNode) -> Option<Self> {
- match node.kind() {
- SyntaxKind::Linebreak => node.cast().map(Self::Linebreak),
- SyntaxKind::Parbreak => node.cast().map(Self::Parbreak),
- SyntaxKind::Text => node.cast().map(Self::Text),
- SyntaxKind::Escape => node.cast().map(Self::Escape),
- SyntaxKind::Shorthand => node.cast().map(Self::Shorthand),
- SyntaxKind::SmartQuote => node.cast().map(Self::SmartQuote),
- SyntaxKind::Strong => node.cast().map(Self::Strong),
- SyntaxKind::Emph => node.cast().map(Self::Emph),
- SyntaxKind::Raw => node.cast().map(Self::Raw),
- SyntaxKind::Link => node.cast().map(Self::Link),
- SyntaxKind::Label => node.cast().map(Self::Label),
- SyntaxKind::Ref => node.cast().map(Self::Ref),
- SyntaxKind::Heading => node.cast().map(Self::Heading),
- SyntaxKind::ListItem => node.cast().map(Self::List),
- SyntaxKind::EnumItem => node.cast().map(Self::Enum),
- SyntaxKind::TermItem => node.cast().map(Self::Term),
- SyntaxKind::Equation => node.cast().map(Self::Equation),
- SyntaxKind::Math => node.cast().map(Self::Math),
- SyntaxKind::MathIdent => node.cast().map(Self::MathIdent),
- SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint),
- SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited),
- SyntaxKind::MathAttach => node.cast().map(Self::MathAttach),
- SyntaxKind::MathFrac => node.cast().map(Self::MathFrac),
- SyntaxKind::MathRoot => node.cast().map(Self::MathRoot),
- SyntaxKind::Ident => node.cast().map(Self::Ident),
- SyntaxKind::None => node.cast().map(Self::None),
- SyntaxKind::Auto => node.cast().map(Self::Auto),
- SyntaxKind::Bool => node.cast().map(Self::Bool),
- SyntaxKind::Int => node.cast().map(Self::Int),
- SyntaxKind::Float => node.cast().map(Self::Float),
- SyntaxKind::Numeric => node.cast().map(Self::Numeric),
- SyntaxKind::Str => node.cast().map(Self::Str),
- SyntaxKind::CodeBlock => node.cast().map(Self::Code),
- SyntaxKind::ContentBlock => node.cast().map(Self::Content),
- SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized),
- SyntaxKind::Array => node.cast().map(Self::Array),
- SyntaxKind::Dict => node.cast().map(Self::Dict),
- SyntaxKind::Unary => node.cast().map(Self::Unary),
- SyntaxKind::Binary => node.cast().map(Self::Binary),
- SyntaxKind::FieldAccess => node.cast().map(Self::FieldAccess),
- SyntaxKind::FuncCall => node.cast().map(Self::FuncCall),
- SyntaxKind::Closure => node.cast().map(Self::Closure),
- SyntaxKind::LetBinding => node.cast().map(Self::Let),
- SyntaxKind::DestructAssignment => node.cast().map(Self::DestructAssign),
- SyntaxKind::SetRule => node.cast().map(Self::Set),
- SyntaxKind::ShowRule => node.cast().map(Self::Show),
- SyntaxKind::Conditional => node.cast().map(Self::Conditional),
- SyntaxKind::WhileLoop => node.cast().map(Self::While),
- SyntaxKind::ForLoop => node.cast().map(Self::For),
- SyntaxKind::ModuleImport => node.cast().map(Self::Import),
- SyntaxKind::ModuleInclude => node.cast().map(Self::Include),
- SyntaxKind::LoopBreak => node.cast().map(Self::Break),
- SyntaxKind::LoopContinue => node.cast().map(Self::Continue),
- SyntaxKind::FuncReturn => node.cast().map(Self::Return),
- _ => Option::None,
- }
- }
-
- fn as_untyped(&self) -> &SyntaxNode {
- match self {
- Self::Text(v) => v.as_untyped(),
- Self::Space(v) => v.as_untyped(),
- Self::Linebreak(v) => v.as_untyped(),
- Self::Parbreak(v) => v.as_untyped(),
- Self::Escape(v) => v.as_untyped(),
- Self::Shorthand(v) => v.as_untyped(),
- Self::SmartQuote(v) => v.as_untyped(),
- Self::Strong(v) => v.as_untyped(),
- Self::Emph(v) => v.as_untyped(),
- Self::Raw(v) => v.as_untyped(),
- Self::Link(v) => v.as_untyped(),
- Self::Label(v) => v.as_untyped(),
- Self::Ref(v) => v.as_untyped(),
- Self::Heading(v) => v.as_untyped(),
- Self::List(v) => v.as_untyped(),
- Self::Enum(v) => v.as_untyped(),
- Self::Term(v) => v.as_untyped(),
- Self::Equation(v) => v.as_untyped(),
- Self::Math(v) => v.as_untyped(),
- Self::MathIdent(v) => v.as_untyped(),
- Self::MathAlignPoint(v) => v.as_untyped(),
- Self::MathDelimited(v) => v.as_untyped(),
- Self::MathAttach(v) => v.as_untyped(),
- Self::MathFrac(v) => v.as_untyped(),
- Self::MathRoot(v) => v.as_untyped(),
- Self::Ident(v) => v.as_untyped(),
- Self::None(v) => v.as_untyped(),
- Self::Auto(v) => v.as_untyped(),
- Self::Bool(v) => v.as_untyped(),
- Self::Int(v) => v.as_untyped(),
- Self::Float(v) => v.as_untyped(),
- Self::Numeric(v) => v.as_untyped(),
- Self::Str(v) => v.as_untyped(),
- Self::Code(v) => v.as_untyped(),
- Self::Content(v) => v.as_untyped(),
- Self::Array(v) => v.as_untyped(),
- Self::Dict(v) => v.as_untyped(),
- Self::Parenthesized(v) => v.as_untyped(),
- Self::Unary(v) => v.as_untyped(),
- Self::Binary(v) => v.as_untyped(),
- Self::FieldAccess(v) => v.as_untyped(),
- Self::FuncCall(v) => v.as_untyped(),
- Self::Closure(v) => v.as_untyped(),
- Self::Let(v) => v.as_untyped(),
- Self::DestructAssign(v) => v.as_untyped(),
- Self::Set(v) => v.as_untyped(),
- Self::Show(v) => v.as_untyped(),
- Self::Conditional(v) => v.as_untyped(),
- Self::While(v) => v.as_untyped(),
- Self::For(v) => v.as_untyped(),
- Self::Import(v) => v.as_untyped(),
- Self::Include(v) => v.as_untyped(),
- Self::Break(v) => v.as_untyped(),
- Self::Continue(v) => v.as_untyped(),
- Self::Return(v) => v.as_untyped(),
- }
- }
-}
-
-impl Expr {
- /// Can this expression be embedded into markup with a hashtag?
- pub fn hashtag(&self) -> bool {
- matches!(
- self,
- Self::Ident(_)
- | Self::None(_)
- | Self::Auto(_)
- | Self::Bool(_)
- | Self::Int(_)
- | Self::Float(_)
- | Self::Numeric(_)
- | Self::Str(_)
- | Self::Code(_)
- | Self::Content(_)
- | Self::Array(_)
- | Self::Dict(_)
- | Self::Parenthesized(_)
- | Self::FieldAccess(_)
- | Self::FuncCall(_)
- | Self::Let(_)
- | Self::Set(_)
- | Self::Show(_)
- | Self::Conditional(_)
- | Self::While(_)
- | Self::For(_)
- | Self::Import(_)
- | Self::Include(_)
- | Self::Break(_)
- | Self::Continue(_)
- | Self::Return(_)
- )
- }
-
- /// Is this a literal?
- pub fn is_literal(&self) -> bool {
- matches!(
- self,
- Self::None(_)
- | Self::Auto(_)
- | Self::Bool(_)
- | Self::Int(_)
- | Self::Float(_)
- | Self::Numeric(_)
- | Self::Str(_)
- )
- }
-}
-
-impl Default for Expr {
- fn default() -> Self {
- Expr::Space(Space::default())
- }
-}
-
-node! {
- /// Plain text without markup.
- Text
-}
-
-impl Text {
- /// Get the text.
- pub fn get(&self) -> &EcoString {
- self.0.text()
- }
-}
-
-node! {
- /// Whitespace in markup or math. Has at most one newline in markup, as more
- /// indicate a paragraph break.
- Space
-}
-
-node! {
- /// A forced line break: `\`.
- Linebreak
-}
-
-node! {
- /// A paragraph break, indicated by one or multiple blank lines.
- Parbreak
-}
-
-node! {
- /// An escape sequence: `\#`, `\u{1F5FA}`.
- Escape
-}
-
-impl Escape {
- /// Get the escaped character.
- pub fn get(&self) -> char {
- let mut s = Scanner::new(self.0.text());
- s.expect('\\');
- if s.eat_if("u{") {
- let hex = s.eat_while(char::is_ascii_hexdigit);
- u32::from_str_radix(hex, 16)
- .ok()
- .and_then(std::char::from_u32)
- .unwrap_or_default()
- } else {
- s.eat().unwrap_or_default()
- }
- }
-}
-
-node! {
- /// A shorthand for a unicode codepoint. For example, `~` for a non-breaking
- /// space or `-?` for a soft hyphen.
- Shorthand
-}
-
-impl Shorthand {
- /// A list of all shorthands.
- pub const LIST: &[(&'static str, char)] = &[
- // Both.
- ("...", '…'),
- // Text only.
- ("~", '\u{00A0}'),
- ("--", '\u{2013}'),
- ("---", '\u{2014}'),
- ("-?", '\u{00AD}'),
- // Math only.
- ("-", '\u{2212}'),
- ("'", '′'),
- ("*", '∗'),
- ("!=", '≠'),
- (":=", '≔'),
- ("::=", '⩴'),
- ("=:", '≕'),
- ("<<", '≪'),
- ("<<<", '⋘'),
- (">>", '≫'),
- (">>>", '⋙'),
- ("<=", '≤'),
- (">=", '≥'),
- ("->", '→'),
- ("-->", '⟶'),
- ("|->", '↦'),
- (">->", '↣'),
- ("->>", '↠'),
- ("<-", '←'),
- ("<--", '⟵'),
- ("<-<", '↢'),
- ("<<-", '↞'),
- ("<->", '↔'),
- ("<-->", '⟷'),
- ("~>", '⇝'),
- ("~~>", '⟿'),
- ("<~", '⇜'),
- ("<~~", '⬳'),
- ("=>", '⇒'),
- ("|=>", '⤇'),
- ("==>", '⟹'),
- ("<==", '⟸'),
- ("<=>", '⇔'),
- ("<==>", '⟺'),
- ("[|", '⟦'),
- ("|]", '⟧'),
- ("||", '‖'),
- ];
-
- /// Get the shorthanded character.
- pub fn get(&self) -> char {
- let text = self.0.text();
- Self::LIST
- .iter()
- .find(|&&(s, _)| s == text)
- .map_or_else(char::default, |&(_, c)| c)
- }
-}
-
-node! {
- /// A smart quote: `'` or `"`.
- SmartQuote
-}
-
-impl SmartQuote {
- /// Whether this is a double quote.
- pub fn double(&self) -> bool {
- self.0.text() == "\""
- }
-}
-
-node! {
- /// Strong content: `*Strong*`.
- Strong
-}
-
-impl Strong {
- /// The contents of the strong node.
- pub fn body(&self) -> Markup {
- self.0.cast_first_match().unwrap_or_default()
- }
-}
-
-node! {
- /// Emphasized content: `_Emphasized_`.
- Emph
-}
-
-impl Emph {
- /// The contents of the emphasis node.
- pub fn body(&self) -> Markup {
- self.0.cast_first_match().unwrap_or_default()
- }
-}
-
-node! {
- /// Raw text with optional syntax highlighting: `` `...` ``.
- Raw
-}
-
-impl Raw {
- /// The trimmed raw text.
- pub fn text(&self) -> EcoString {
- let mut text = self.0.text().as_str();
- let blocky = text.starts_with("```");
- text = text.trim_matches('`');
-
- // Trim tag, one space at the start, and one space at the end if the
- // last non-whitespace char is a backtick.
- if blocky {
- let mut s = Scanner::new(text);
- if s.eat_if(is_id_start) {
- s.eat_while(is_id_continue);
- }
- text = s.after();
- text = text.strip_prefix(' ').unwrap_or(text);
- if text.trim_end().ends_with('`') {
- text = text.strip_suffix(' ').unwrap_or(text);
- }
- }
-
- // Split into lines.
- let mut lines = split_newlines(text);
-
- if blocky {
- let dedent = lines
- .iter()
- .skip(1)
- .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
- .min()
- .unwrap_or(0);
-
- // Dedent based on column, but not for the first line.
- for line in lines.iter_mut().skip(1) {
- let offset = line.chars().take(dedent).map(char::len_utf8).sum();
- *line = &line[offset..];
- }
-
- let is_whitespace = |line: &&str| line.chars().all(char::is_whitespace);
-
- // Trims a sequence of whitespace followed by a newline at the start.
- if lines.first().map_or(false, is_whitespace) {
- lines.remove(0);
- }
-
- // Trims a newline followed by a sequence of whitespace at the end.
- if lines.last().map_or(false, is_whitespace) {
- lines.pop();
- }
- }
-
- lines.join("\n").into()
- }
-
- /// An optional identifier specifying the language to syntax-highlight in.
- pub fn lang(&self) -> Option<&str> {
- let text = self.0.text();
-
- // Only blocky literals are supposed to contain a language.
- if !text.starts_with("```") {
- return Option::None;
- }
-
- let inner = text.trim_start_matches('`');
- let mut s = Scanner::new(inner);
- s.eat_if(is_id_start).then(|| {
- s.eat_while(is_id_continue);
- s.before()
- })
- }
-
- /// Whether the raw text should be displayed in a separate block.
- pub fn block(&self) -> bool {
- let text = self.0.text();
- text.starts_with("```") && text.chars().any(is_newline)
- }
-}
-
-node! {
- /// A hyperlink: `https://typst.org`.
- Link
-}
-
-impl Link {
- /// Get the URL.
- pub fn get(&self) -> &EcoString {
- self.0.text()
- }
-}
-
-node! {
- /// A label: `<intro>`.
- Label
-}
-
-impl Label {
- /// Get the label's text.
- pub fn get(&self) -> &str {
- self.0.text().trim_start_matches('<').trim_end_matches('>')
- }
-}
-
-node! {
- /// A reference: `@target`, `@target[..]`.
- Ref
-}
-
-impl Ref {
- /// Get the target.
- pub fn target(&self) -> &str {
- self.0
- .children()
- .find(|node| node.kind() == SyntaxKind::RefMarker)
- .map(|node| node.text().trim_start_matches('@'))
- .unwrap_or_default()
- }
-
- /// Get the supplement.
- pub fn supplement(&self) -> Option<ContentBlock> {
- self.0.cast_last_match()
- }
-}
-
-node! {
- /// A section heading: `= Introduction`.
- Heading
-}
-
-impl Heading {
- /// The contents of the heading.
- pub fn body(&self) -> Markup {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The section depth (number of equals signs).
- pub fn level(&self) -> NonZeroUsize {
- self.0
- .children()
- .find(|node| node.kind() == SyntaxKind::HeadingMarker)
- .and_then(|node| node.len().try_into().ok())
- .unwrap_or(NonZeroUsize::ONE)
- }
-}
-
-node! {
- /// An item in a bullet list: `- ...`.
- ListItem
-}
-
-impl ListItem {
- /// The contents of the list item.
- pub fn body(&self) -> Markup {
- self.0.cast_first_match().unwrap_or_default()
- }
-}
-
-node! {
- /// An item in an enumeration (numbered list): `+ ...` or `1. ...`.
- EnumItem
-}
-
-impl EnumItem {
- /// The explicit numbering, if any: `23.`.
- pub fn number(&self) -> Option<usize> {
- self.0.children().find_map(|node| match node.kind() {
- SyntaxKind::EnumMarker => node.text().trim_end_matches('.').parse().ok(),
- _ => Option::None,
- })
- }
-
- /// The contents of the list item.
- pub fn body(&self) -> Markup {
- self.0.cast_first_match().unwrap_or_default()
- }
-}
-
-node! {
- /// An item in a term list: `/ Term: Details`.
- TermItem
-}
-
-impl TermItem {
- /// The term described by the item.
- pub fn term(&self) -> Markup {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The description of the term.
- pub fn description(&self) -> Markup {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A mathemathical equation: `$x$`, `$ x^2 $`.
- Equation
-}
-
-impl Equation {
- /// The contained math.
- pub fn body(&self) -> Math {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// Whether the equation should be displayed as a separate block.
- pub fn block(&self) -> bool {
- let is_space = |node: Option<&SyntaxNode>| {
- node.map(SyntaxNode::kind) == Some(SyntaxKind::Space)
- };
- is_space(self.0.children().nth(1)) && is_space(self.0.children().nth_back(1))
- }
-}
-
-node! {
- /// The contents of a mathematical equation: `x^2 + 1`.
- Math
-}
-
-impl Math {
- /// The expressions the mathematical content consists of.
- pub fn exprs(&self) -> impl DoubleEndedIterator<Item = Expr> + '_ {
- self.0.children().filter_map(Expr::cast_with_space)
- }
-}
-
-node! {
- /// An identifier in math: `pi`.
- MathIdent
-}
-
-impl MathIdent {
- /// Get the identifier.
- pub fn get(&self) -> &EcoString {
- self.0.text()
- }
-
- /// Take out the contained identifier.
- pub fn take(self) -> EcoString {
- self.0.into_text()
- }
-
- /// Get the identifier as a string slice.
- pub fn as_str(&self) -> &str {
- self.get()
- }
-}
-
-impl Deref for MathIdent {
- type Target = str;
-
- fn deref(&self) -> &Self::Target {
- self.as_str()
- }
-}
-
-node! {
- /// An alignment point in math: `&`.
- MathAlignPoint
-}
-
-node! {
- /// Matched delimiters in math: `[x + y]`.
- MathDelimited
-}
-
-impl MathDelimited {
- /// The opening delimiter.
- pub fn open(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The contents, including the delimiters.
- pub fn body(&self) -> Math {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The closing delimiter.
- pub fn close(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A base with optional attachments in math: `a_1^2`.
- MathAttach
-}
-
-impl MathAttach {
- /// The base, to which things are attached.
- pub fn base(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The bottom attachment.
- pub fn bottom(&self) -> Option<Expr> {
- self.0
- .children()
- .skip_while(|node| !matches!(node.kind(), SyntaxKind::Underscore))
- .find_map(SyntaxNode::cast)
- }
-
- /// The top attachment.
- pub fn top(&self) -> Option<Expr> {
- self.0
- .children()
- .skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat))
- .find_map(SyntaxNode::cast)
- }
-}
-
-node! {
- /// A fraction in math: `x/2`
- MathFrac
-}
-
-impl MathFrac {
- /// The numerator.
- pub fn num(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The denominator.
- pub fn denom(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A root in math: `√x`, `∛x` or `∜x`.
- MathRoot
-}
-
-impl MathRoot {
- /// The index of the root.
- pub fn index(&self) -> Option<usize> {
- match self.0.children().next().map(|node| node.text().as_str()) {
- Some("∜") => Some(4),
- Some("∛") => Some(3),
- Some("√") => Option::None,
- _ => Option::None,
- }
- }
-
- /// The radicand.
- pub fn radicand(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-}
-
-node! {
- /// An identifier: `it`.
- Ident
-}
-
-impl Ident {
- /// Get the identifier.
- pub fn get(&self) -> &EcoString {
- self.0.text()
- }
-
- /// Take out the contained identifier.
- pub fn take(self) -> EcoString {
- self.0.into_text()
- }
-
- /// Get the identifier as a string slice.
- pub fn as_str(&self) -> &str {
- self.get()
- }
-}
-
-impl Deref for Ident {
- type Target = str;
-
- fn deref(&self) -> &Self::Target {
- self.as_str()
- }
-}
-
-node! {
- /// The `none` literal.
- None
-}
-
-node! {
- /// The `auto` literal.
- Auto
-}
-
-node! {
- /// A boolean: `true`, `false`.
- Bool
-}
-
-impl Bool {
- /// Get the boolean value.
- pub fn get(&self) -> bool {
- self.0.text() == "true"
- }
-}
-
-node! {
- /// An integer: `120`.
- Int
-}
-
-impl Int {
- /// Get the integer value.
- pub fn get(&self) -> i64 {
- let text = self.0.text();
- if let Some(rest) = text.strip_prefix("0x") {
- i64::from_str_radix(rest, 16)
- } else if let Some(rest) = text.strip_prefix("0o") {
- i64::from_str_radix(rest, 8)
- } else if let Some(rest) = text.strip_prefix("0b") {
- i64::from_str_radix(rest, 2)
- } else {
- text.parse()
- }
- .unwrap_or_default()
- }
-}
-
-node! {
- /// A floating-point number: `1.2`, `10e-4`.
- Float
-}
-
-impl Float {
- /// Get the floating-point value.
- pub fn get(&self) -> f64 {
- self.0.text().parse().unwrap_or_default()
- }
-}
-
-node! {
- /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`.
- Numeric
-}
-
-impl Numeric {
- /// Get the numeric value and unit.
- pub fn get(&self) -> (f64, Unit) {
- let text = self.0.text();
- let count = text
- .chars()
- .rev()
- .take_while(|c| matches!(c, 'a'..='z' | '%'))
- .count();
-
- let split = text.len() - count;
- let value = text[..split].parse().unwrap_or_default();
- let unit = match &text[split..] {
- "pt" => Unit::Length(AbsUnit::Pt),
- "mm" => Unit::Length(AbsUnit::Mm),
- "cm" => Unit::Length(AbsUnit::Cm),
- "in" => Unit::Length(AbsUnit::In),
- "deg" => Unit::Angle(AngleUnit::Deg),
- "rad" => Unit::Angle(AngleUnit::Rad),
- "em" => Unit::Em,
- "fr" => Unit::Fr,
- "%" => Unit::Percent,
- _ => Unit::Percent,
- };
-
- (value, unit)
- }
-}
-
-/// Unit of a numeric value.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Unit {
- /// An absolute length unit.
- Length(AbsUnit),
- /// An angular unit.
- Angle(AngleUnit),
- /// Font-relative: `1em` is the same as the font size.
- Em,
- /// Fractions: `fr`.
- Fr,
- /// Percentage: `%`.
- Percent,
-}
-
-node! {
- /// A quoted string: `"..."`.
- Str
-}
-
-impl Str {
- /// Get the string value with resolved escape sequences.
- pub fn get(&self) -> EcoString {
- let text = self.0.text();
- let unquoted = &text[1..text.len() - 1];
- if !unquoted.contains('\\') {
- return unquoted.into();
- }
-
- let mut out = EcoString::with_capacity(unquoted.len());
- let mut s = Scanner::new(unquoted);
-
- while let Some(c) = s.eat() {
- if c != '\\' {
- out.push(c);
- continue;
- }
-
- let start = s.locate(-1);
- match s.eat() {
- Some('\\') => out.push('\\'),
- Some('"') => out.push('"'),
- Some('n') => out.push('\n'),
- Some('r') => out.push('\r'),
- Some('t') => out.push('\t'),
- Some('u') if s.eat_if('{') => {
- let sequence = s.eat_while(char::is_ascii_hexdigit);
- s.eat_if('}');
-
- match u32::from_str_radix(sequence, 16)
- .ok()
- .and_then(std::char::from_u32)
- {
- Some(c) => out.push(c),
- Option::None => out.push_str(s.from(start)),
- }
- }
- _ => out.push_str(s.from(start)),
- }
- }
-
- out
- }
-}
-
-node! {
- /// A code block: `{ let x = 1; x + 2 }`.
- CodeBlock
-}
-
-impl CodeBlock {
- /// The contained code.
- pub fn body(&self) -> Code {
- self.0.cast_first_match().unwrap_or_default()
- }
-}
-
-node! {
- /// Code.
- Code
-}
-
-impl Code {
- /// The list of expressions contained in the code.
- pub fn exprs(&self) -> impl DoubleEndedIterator<Item = Expr> + '_ {
- self.0.children().filter_map(SyntaxNode::cast)
- }
-}
-
-node! {
- /// A content block: `[*Hi* there!]`.
- ContentBlock
-}
-
-impl ContentBlock {
- /// The contained markup.
- pub fn body(&self) -> Markup {
- self.0.cast_first_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A grouped expression: `(1 + 2)`.
- Parenthesized
-}
-
-impl Parenthesized {
- /// The wrapped expression.
- pub fn expr(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-}
-
-node! {
- /// An array: `(1, "hi", 12cm)`.
- Array
-}
-
-impl Array {
- /// The array's items.
- pub fn items(&self) -> impl DoubleEndedIterator<Item = ArrayItem> + '_ {
- self.0.children().filter_map(SyntaxNode::cast)
- }
-}
-
-/// An item in an array.
-#[derive(Debug, Clone, Hash)]
-pub enum ArrayItem {
- /// A bare expression: `12`.
- Pos(Expr),
- /// A spread expression: `..things`.
- Spread(Expr),
-}
-
-impl AstNode for ArrayItem {
- fn from_untyped(node: &SyntaxNode) -> Option<Self> {
- match node.kind() {
- SyntaxKind::Spread => node.cast_first_match().map(Self::Spread),
- _ => node.cast().map(Self::Pos),
- }
- }
-
- fn as_untyped(&self) -> &SyntaxNode {
- match self {
- Self::Pos(v) => v.as_untyped(),
- Self::Spread(v) => v.as_untyped(),
- }
- }
-}
-
-node! {
- /// A dictionary: `(thickness: 3pt, pattern: dashed)`.
- Dict
-}
-
-impl Dict {
- /// The dictionary's items.
- pub fn items(&self) -> impl DoubleEndedIterator<Item = DictItem> + '_ {
- self.0.children().filter_map(SyntaxNode::cast)
- }
-}
-
-/// An item in an dictionary expression.
-#[derive(Debug, Clone, Hash)]
-pub enum DictItem {
- /// A named pair: `thickness: 3pt`.
- Named(Named),
- /// A keyed pair: `"spacy key": true`.
- Keyed(Keyed),
- /// A spread expression: `..things`.
- Spread(Expr),
-}
-
-impl AstNode for DictItem {
- fn from_untyped(node: &SyntaxNode) -> Option<Self> {
- match node.kind() {
- SyntaxKind::Named => node.cast().map(Self::Named),
- SyntaxKind::Keyed => node.cast().map(Self::Keyed),
- SyntaxKind::Spread => node.cast_first_match().map(Self::Spread),
- _ => Option::None,
- }
- }
-
- fn as_untyped(&self) -> &SyntaxNode {
- match self {
- Self::Named(v) => v.as_untyped(),
- Self::Keyed(v) => v.as_untyped(),
- Self::Spread(v) => v.as_untyped(),
- }
- }
-}
-
-node! {
- /// A named pair: `thickness: 3pt`.
- Named
-}
-
-impl Named {
- /// The name: `thickness`.
- pub fn name(&self) -> Ident {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The right-hand side of the pair: `3pt`.
- pub fn expr(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-
- /// The right-hand side of the pair as an identifier.
- pub fn expr_ident(&self) -> Option<Ident> {
- self.0.cast_last_match()
- }
-}
-
-node! {
- /// A keyed pair: `"spacy key": true`.
- Keyed
-}
-
-impl Keyed {
- /// The key: `"spacy key"`.
- pub fn key(&self) -> Str {
- self.0
- .children()
- .find_map(|node| node.cast::<Str>())
- .unwrap_or_default()
- }
-
- /// The right-hand side of the pair: `true`.
- pub fn expr(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A unary operation: `-x`.
- Unary
-}
-
-impl Unary {
- /// The operator: `-`.
- pub fn op(&self) -> UnOp {
- self.0
- .children()
- .find_map(|node| UnOp::from_kind(node.kind()))
- .unwrap_or(UnOp::Pos)
- }
-
- /// The expression to operate on: `x`.
- pub fn expr(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-/// A unary operator.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum UnOp {
- /// The plus operator: `+`.
- Pos,
- /// The negation operator: `-`.
- Neg,
- /// The boolean `not`.
- Not,
-}
-
-impl UnOp {
- /// Try to convert the token into a unary operation.
- pub fn from_kind(token: SyntaxKind) -> Option<Self> {
- Some(match token {
- SyntaxKind::Plus => Self::Pos,
- SyntaxKind::Minus => Self::Neg,
- SyntaxKind::Not => Self::Not,
- _ => return Option::None,
- })
- }
-
- /// The precedence of this operator.
- pub fn precedence(self) -> usize {
- match self {
- Self::Pos | Self::Neg => 7,
- Self::Not => 4,
- }
- }
-
- /// The string representation of this operation.
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Pos => "+",
- Self::Neg => "-",
- Self::Not => "not",
- }
- }
-}
-
-node! {
- /// A binary operation: `a + b`.
- Binary
-}
-
-impl Binary {
- /// The binary operator: `+`.
- pub fn op(&self) -> BinOp {
- let mut not = false;
- self.0
- .children()
- .find_map(|node| match node.kind() {
- SyntaxKind::Not => {
- not = true;
- Option::None
- }
- SyntaxKind::In if not => Some(BinOp::NotIn),
- _ => BinOp::from_kind(node.kind()),
- })
- .unwrap_or(BinOp::Add)
- }
-
- /// The left-hand side of the operation: `a`.
- pub fn lhs(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The right-hand side of the operation: `b`.
- pub fn rhs(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-/// A binary operator.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum BinOp {
- /// The addition operator: `+`.
- Add,
- /// The subtraction operator: `-`.
- Sub,
- /// The multiplication operator: `*`.
- Mul,
- /// The division operator: `/`.
- Div,
- /// The short-circuiting boolean `and`.
- And,
- /// The short-circuiting boolean `or`.
- Or,
- /// The equality operator: `==`.
- Eq,
- /// The inequality operator: `!=`.
- Neq,
- /// The less-than operator: `<`.
- Lt,
- /// The less-than or equal operator: `<=`.
- Leq,
- /// The greater-than operator: `>`.
- Gt,
- /// The greater-than or equal operator: `>=`.
- Geq,
- /// The assignment operator: `=`.
- Assign,
- /// The containment operator: `in`.
- In,
- /// The inversed containment operator: `not in`.
- NotIn,
- /// The add-assign operator: `+=`.
- AddAssign,
- /// The subtract-assign oeprator: `-=`.
- SubAssign,
- /// The multiply-assign operator: `*=`.
- MulAssign,
- /// The divide-assign operator: `/=`.
- DivAssign,
-}
-
-impl BinOp {
- /// Try to convert the token into a binary operation.
- pub fn from_kind(token: SyntaxKind) -> Option<Self> {
- Some(match token {
- SyntaxKind::Plus => Self::Add,
- SyntaxKind::Minus => Self::Sub,
- SyntaxKind::Star => Self::Mul,
- SyntaxKind::Slash => Self::Div,
- SyntaxKind::And => Self::And,
- SyntaxKind::Or => Self::Or,
- SyntaxKind::EqEq => Self::Eq,
- SyntaxKind::ExclEq => Self::Neq,
- SyntaxKind::Lt => Self::Lt,
- SyntaxKind::LtEq => Self::Leq,
- SyntaxKind::Gt => Self::Gt,
- SyntaxKind::GtEq => Self::Geq,
- SyntaxKind::Eq => Self::Assign,
- SyntaxKind::In => Self::In,
- SyntaxKind::PlusEq => Self::AddAssign,
- SyntaxKind::HyphEq => Self::SubAssign,
- SyntaxKind::StarEq => Self::MulAssign,
- SyntaxKind::SlashEq => Self::DivAssign,
- _ => return Option::None,
- })
- }
-
- /// The precedence of this operator.
- pub fn precedence(self) -> usize {
- match self {
- Self::Mul => 6,
- Self::Div => 6,
- Self::Add => 5,
- Self::Sub => 5,
- Self::Eq => 4,
- Self::Neq => 4,
- Self::Lt => 4,
- Self::Leq => 4,
- Self::Gt => 4,
- Self::Geq => 4,
- Self::In => 4,
- Self::NotIn => 4,
- Self::And => 3,
- Self::Or => 2,
- Self::Assign => 1,
- Self::AddAssign => 1,
- Self::SubAssign => 1,
- Self::MulAssign => 1,
- Self::DivAssign => 1,
- }
- }
-
- /// The associativity of this operator.
- pub fn assoc(self) -> Assoc {
- match self {
- Self::Add => Assoc::Left,
- Self::Sub => Assoc::Left,
- Self::Mul => Assoc::Left,
- Self::Div => Assoc::Left,
- Self::And => Assoc::Left,
- Self::Or => Assoc::Left,
- Self::Eq => Assoc::Left,
- Self::Neq => Assoc::Left,
- Self::Lt => Assoc::Left,
- Self::Leq => Assoc::Left,
- Self::Gt => Assoc::Left,
- Self::Geq => Assoc::Left,
- Self::In => Assoc::Left,
- Self::NotIn => Assoc::Left,
- Self::Assign => Assoc::Right,
- Self::AddAssign => Assoc::Right,
- Self::SubAssign => Assoc::Right,
- Self::MulAssign => Assoc::Right,
- Self::DivAssign => Assoc::Right,
- }
- }
-
- /// The string representation of this operation.
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Add => "+",
- Self::Sub => "-",
- Self::Mul => "*",
- Self::Div => "/",
- Self::And => "and",
- Self::Or => "or",
- Self::Eq => "==",
- Self::Neq => "!=",
- Self::Lt => "<",
- Self::Leq => "<=",
- Self::Gt => ">",
- Self::Geq => ">=",
- Self::In => "in",
- Self::NotIn => "not in",
- Self::Assign => "=",
- Self::AddAssign => "+=",
- Self::SubAssign => "-=",
- Self::MulAssign => "*=",
- Self::DivAssign => "/=",
- }
- }
-}
-
-/// The associativity of a binary operator.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Assoc {
- /// Left-associative: `a + b + c` is equivalent to `(a + b) + c`.
- Left,
- /// Right-associative: `a = b = c` is equivalent to `a = (b = c)`.
- Right,
-}
-
-node! {
- /// A field access: `properties.age`.
- FieldAccess
-}
-
-impl FieldAccess {
- /// The expression to access the field on.
- pub fn target(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The name of the field.
- pub fn field(&self) -> Ident {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// An invocation of a function or method: `f(x, y)`.
- FuncCall
-}
-
-impl FuncCall {
- /// The function to call.
- pub fn callee(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The arguments to the function.
- pub fn args(&self) -> Args {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A function call's argument list: `(12pt, y)`.
- Args
-}
-
-impl Args {
- /// The positional and named arguments.
- pub fn items(&self) -> impl DoubleEndedIterator<Item = Arg> + '_ {
- self.0.children().filter_map(SyntaxNode::cast)
- }
-}
-
-/// An argument to a function call.
-#[derive(Debug, Clone, Hash)]
-pub enum Arg {
- /// A positional argument: `12`.
- Pos(Expr),
- /// A named argument: `draw: false`.
- Named(Named),
- /// A spread argument: `..things`.
- Spread(Expr),
-}
-
-impl AstNode for Arg {
- fn from_untyped(node: &SyntaxNode) -> Option<Self> {
- match node.kind() {
- SyntaxKind::Named => node.cast().map(Self::Named),
- SyntaxKind::Spread => node.cast_first_match().map(Self::Spread),
- _ => node.cast().map(Self::Pos),
- }
- }
-
- fn as_untyped(&self) -> &SyntaxNode {
- match self {
- Self::Pos(v) => v.as_untyped(),
- Self::Named(v) => v.as_untyped(),
- Self::Spread(v) => v.as_untyped(),
- }
- }
-}
-
-node! {
- /// A closure: `(x, y) => z`.
- Closure
-}
-
-impl Closure {
- /// The name of the closure.
- ///
- /// This only exists if you use the function syntax sugar: `let f(x) = y`.
- pub fn name(&self) -> Option<Ident> {
- self.0.children().next()?.cast()
- }
-
- /// The parameter bindings.
- pub fn params(&self) -> Params {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The body of the closure.
- pub fn body(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A closure's parameters: `(x, y)`.
- Params
-}
-
-impl Params {
- /// The parameter bindings.
- pub fn children(&self) -> impl DoubleEndedIterator<Item = Param> + '_ {
- self.0.children().filter_map(SyntaxNode::cast)
- }
-}
-
-node! {
- /// A spread: `..x` or `..x.at(0)`.
- Spread
-}
-
-impl Spread {
- /// Try to get an identifier.
- pub fn name(&self) -> Option<Ident> {
- self.0.cast_first_match()
- }
-
- /// Try to get an expression.
- pub fn expr(&self) -> Option<Expr> {
- self.0.cast_first_match()
- }
-}
-
-node! {
- /// An underscore: `_`
- Underscore
-}
-
-/// A parameter to a closure.
-#[derive(Debug, Clone, Hash)]
-pub enum Param {
- /// A positional parameter: `x`.
- Pos(Pattern),
- /// A named parameter with a default value: `draw: false`.
- Named(Named),
- /// An argument sink: `..args`.
- Sink(Spread),
-}
-
-impl AstNode for Param {
- fn from_untyped(node: &SyntaxNode) -> Option<Self> {
- match node.kind() {
- SyntaxKind::Named => node.cast().map(Self::Named),
- SyntaxKind::Spread => node.cast().map(Self::Sink),
- _ => node.cast().map(Self::Pos),
- }
- }
-
- fn as_untyped(&self) -> &SyntaxNode {
- match self {
- Self::Pos(v) => v.as_untyped(),
- Self::Named(v) => v.as_untyped(),
- Self::Sink(v) => v.as_untyped(),
- }
- }
-}
-
-node! {
- /// A destructuring pattern: `x` or `(x, _, ..y)`.
- Destructuring
-}
-
-impl Destructuring {
- /// The bindings of the destructuring.
- pub fn bindings(&self) -> impl Iterator<Item = DestructuringKind> + '_ {
- self.0.children().filter_map(SyntaxNode::cast)
- }
-
- // Returns a list of all identifiers in the pattern.
- pub fn idents(&self) -> impl Iterator<Item = Ident> + '_ {
- self.bindings().filter_map(|binding| match binding {
- DestructuringKind::Normal(Expr::Ident(ident)) => Some(ident),
- DestructuringKind::Sink(spread) => spread.name(),
- DestructuringKind::Named(named) => named.expr_ident(),
- _ => Option::None,
- })
- }
-}
-
-/// The kind of an element in a destructuring pattern.
-#[derive(Debug, Clone, Hash)]
-pub enum DestructuringKind {
- /// An expression: `x`.
- Normal(Expr),
- /// An argument sink: `..y`.
- Sink(Spread),
- /// Named arguments: `x: 1`.
- Named(Named),
- /// A placeholder: `_`.
- Placeholder(Underscore),
-}
-
-impl AstNode for DestructuringKind {
- fn from_untyped(node: &SyntaxNode) -> Option<Self> {
- match node.kind() {
- SyntaxKind::Named => node.cast().map(Self::Named),
- SyntaxKind::Spread => node.cast().map(Self::Sink),
- SyntaxKind::Underscore => node.cast().map(Self::Placeholder),
- _ => node.cast().map(Self::Normal),
- }
- }
-
- fn as_untyped(&self) -> &SyntaxNode {
- match self {
- Self::Normal(v) => v.as_untyped(),
- Self::Named(v) => v.as_untyped(),
- Self::Sink(v) => v.as_untyped(),
- Self::Placeholder(v) => v.as_untyped(),
- }
- }
-}
-
-/// The kind of a pattern.
-#[derive(Debug, Clone, Hash)]
-pub enum Pattern {
- /// A single expression: `x`.
- Normal(Expr),
- /// A placeholder: `_`.
- Placeholder(Underscore),
- /// A destructuring pattern: `(x, _, ..y)`.
- Destructuring(Destructuring),
-}
-
-impl AstNode for Pattern {
- fn from_untyped(node: &SyntaxNode) -> Option<Self> {
- match node.kind() {
- SyntaxKind::Destructuring => node.cast().map(Self::Destructuring),
- SyntaxKind::Underscore => node.cast().map(Self::Placeholder),
- _ => node.cast().map(Self::Normal),
- }
- }
-
- fn as_untyped(&self) -> &SyntaxNode {
- match self {
- Self::Normal(v) => v.as_untyped(),
- Self::Destructuring(v) => v.as_untyped(),
- Self::Placeholder(v) => v.as_untyped(),
- }
- }
-}
-
-impl Pattern {
- // Returns a list of all identifiers in the pattern.
- pub fn idents(&self) -> Vec<Ident> {
- match self {
- Pattern::Normal(Expr::Ident(ident)) => vec![ident.clone()],
- Pattern::Destructuring(destruct) => destruct.idents().collect(),
- _ => vec![],
- }
- }
-}
-
-impl Default for Pattern {
- fn default() -> Self {
- Self::Normal(Expr::default())
- }
-}
-
-node! {
- /// A let binding: `let x = 1`.
- LetBinding
-}
-
-#[derive(Debug)]
-pub enum LetBindingKind {
- /// A normal binding: `let x = 1`.
- Normal(Pattern),
- /// A closure binding: `let f(x) = 1`.
- Closure(Ident),
-}
-
-impl LetBindingKind {
- // Returns a list of all identifiers in the pattern.
- pub fn idents(&self) -> Vec<Ident> {
- match self {
- LetBindingKind::Normal(pattern) => pattern.idents(),
- LetBindingKind::Closure(ident) => {
- vec![ident.clone()]
- }
- }
- }
-}
-
-impl LetBinding {
- /// The kind of the let binding.
- pub fn kind(&self) -> LetBindingKind {
- match self.0.cast_first_match::<Pattern>() {
- Some(Pattern::Normal(Expr::Closure(closure))) => {
- LetBindingKind::Closure(closure.name().unwrap_or_default())
- }
- pattern => LetBindingKind::Normal(pattern.unwrap_or_default()),
- }
- }
-
- /// The expression the binding is initialized with.
- pub fn init(&self) -> Option<Expr> {
- match self.kind() {
- LetBindingKind::Normal(Pattern::Normal(_)) => {
- self.0.children().filter_map(SyntaxNode::cast).nth(1)
- }
- LetBindingKind::Normal(_) => self.0.cast_first_match(),
- LetBindingKind::Closure(_) => self.0.cast_first_match(),
- }
- }
-}
-
-node! {
- /// An assignment expression `(x, y) = (1, 2)`.
- DestructAssignment
-}
-
-impl DestructAssignment {
- /// The pattern of the assignment.
- pub fn pattern(&self) -> Pattern {
- self.0.cast_first_match::<Pattern>().unwrap_or_default()
- }
-
- /// The expression that is assigned.
- pub fn value(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A set rule: `set text(...)`.
- SetRule
-}
-
-impl SetRule {
- /// The function to set style properties for.
- pub fn target(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The style properties to set.
- pub fn args(&self) -> Args {
- self.0.cast_last_match().unwrap_or_default()
- }
-
- /// A condition under which the set rule applies.
- pub fn condition(&self) -> Option<Expr> {
- self.0
- .children()
- .skip_while(|child| child.kind() != SyntaxKind::If)
- .find_map(SyntaxNode::cast)
- }
-}
-
-node! {
- /// A show rule: `show heading: it => emph(it.body)`.
- ShowRule
-}
-
-impl ShowRule {
- /// Defines which nodes the show rule applies to.
- pub fn selector(&self) -> Option<Expr> {
- self.0
- .children()
- .rev()
- .skip_while(|child| child.kind() != SyntaxKind::Colon)
- .find_map(SyntaxNode::cast)
- }
-
- /// The transformation recipe.
- pub fn transform(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// An if-else conditional: `if x { y } else { z }`.
- Conditional
-}
-
-impl Conditional {
- /// The condition which selects the body to evaluate.
- pub fn condition(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The expression to evaluate if the condition is true.
- pub fn if_body(&self) -> Expr {
- self.0
- .children()
- .filter_map(SyntaxNode::cast)
- .nth(1)
- .unwrap_or_default()
- }
-
- /// The expression to evaluate if the condition is false.
- pub fn else_body(&self) -> Option<Expr> {
- self.0.children().filter_map(SyntaxNode::cast).nth(2)
- }
-}
-
-node! {
- /// A while loop: `while x { y }`.
- WhileLoop
-}
-
-impl WhileLoop {
- /// The condition which selects whether to evaluate the body.
- pub fn condition(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The expression to evaluate while the condition is true.
- pub fn body(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A for loop: `for x in y { z }`.
- ForLoop
-}
-
-impl ForLoop {
- /// The pattern to assign to.
- pub fn pattern(&self) -> Pattern {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The expression to iterate over.
- pub fn iter(&self) -> Expr {
- self.0
- .children()
- .skip_while(|&c| c.kind() != SyntaxKind::In)
- .find_map(SyntaxNode::cast)
- .unwrap_or_default()
- }
-
- /// The expression to evaluate for each iteration.
- pub fn body(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A module import: `import "utils.typ": a, b, c`.
- ModuleImport
-}
-
-impl ModuleImport {
- /// The module or path from which the items should be imported.
- pub fn source(&self) -> Expr {
- self.0.cast_first_match().unwrap_or_default()
- }
-
- /// The items to be imported.
- pub fn imports(&self) -> Option<Imports> {
- self.0.children().find_map(|node| match node.kind() {
- SyntaxKind::Star => Some(Imports::Wildcard),
- SyntaxKind::ImportItems => {
- let items = node.children().filter_map(SyntaxNode::cast).collect();
- Some(Imports::Items(items))
- }
- _ => Option::None,
- })
- }
-}
-
-/// The items that ought to be imported from a file.
-#[derive(Debug, Clone, Hash)]
-pub enum Imports {
- /// All items in the scope of the file should be imported.
- Wildcard,
- /// The specified items from the file should be imported.
- Items(Vec<Ident>),
-}
-
-node! {
- /// A module include: `include "chapter1.typ"`.
- ModuleInclude
-}
-
-impl ModuleInclude {
- /// The module or path from which the content should be included.
- pub fn source(&self) -> Expr {
- self.0.cast_last_match().unwrap_or_default()
- }
-}
-
-node! {
- /// A break from a loop: `break`.
- LoopBreak
-}
-
-node! {
- /// A continue in a loop: `continue`.
- LoopContinue
-}
-
-node! {
- /// A return from a function: `return`, `return x + 1`.
- FuncReturn
-}
-
-impl FuncReturn {
- /// The expression to return.
- pub fn body(&self) -> Option<Expr> {
- self.0.cast_last_match()
- }
-}
diff --git a/src/syntax/kind.rs b/src/syntax/kind.rs
deleted file mode 100644
index 26e949ca..00000000
--- a/src/syntax/kind.rs
+++ /dev/null
@@ -1,448 +0,0 @@
-/// A syntactical building block of a Typst file.
-///
-/// Can be created by the lexer or by the parser.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-#[repr(u8)]
-pub enum SyntaxKind {
- /// Markup.
- Markup,
- /// Plain text without markup.
- Text,
- /// Whitespace. Contains at most one newline in markup, as more indicate a
- /// paragraph break.
- Space,
- /// A forced line break: `\`.
- Linebreak,
- /// A paragraph break, indicated by one or multiple blank lines.
- Parbreak,
- /// An escape sequence: `\#`, `\u{1F5FA}`.
- Escape,
- /// A shorthand for a unicode codepoint. For example, `~` for non-breaking
- /// space or `-?` for a soft hyphen.
- Shorthand,
- /// A smart quote: `'` or `"`.
- SmartQuote,
- /// Strong content: `*Strong*`.
- Strong,
- /// Emphasized content: `_Emphasized_`.
- Emph,
- /// Raw text with optional syntax highlighting: `` `...` ``.
- Raw,
- /// A hyperlink: `https://typst.org`.
- Link,
- /// A label: `<intro>`.
- Label,
- /// A reference: `@target`, `@target[..]`.
- Ref,
- /// Introduces a reference: `@target`.
- RefMarker,
- /// A section heading: `= Introduction`.
- Heading,
- /// Introduces a section heading: `=`, `==`, ...
- HeadingMarker,
- /// An item in a bullet list: `- ...`.
- ListItem,
- /// Introduces a list item: `-`.
- ListMarker,
- /// An item in an enumeration (numbered list): `+ ...` or `1. ...`.
- EnumItem,
- /// Introduces an enumeration item: `+`, `1.`.
- EnumMarker,
- /// An item in a term list: `/ Term: Details`.
- TermItem,
- /// Introduces a term item: `/`.
- TermMarker,
- /// A mathematical equation: `$x$`, `$ x^2 $`.
- Equation,
-
- /// The contents of a mathematical equation: `x^2 + 1`.
- Math,
- /// An identifier in math: `pi`.
- MathIdent,
- /// An alignment point in math: `&`.
- MathAlignPoint,
- /// Matched delimiters in math: `[x + y]`.
- MathDelimited,
- /// A base with optional attachments in math: `a_1^2`.
- MathAttach,
- /// A fraction in math: `x/2`.
- MathFrac,
- /// A root in math: `√x`, `∛x` or `∜x`.
- MathRoot,
-
- /// A hashtag that switches into code mode: `#`.
- Hashtag,
- /// A left curly brace, starting a code block: `{`.
- LeftBrace,
- /// A right curly brace, terminating a code block: `}`.
- RightBrace,
- /// A left square bracket, starting a content block: `[`.
- LeftBracket,
- /// A right square bracket, terminating a content block: `]`.
- RightBracket,
- /// A left round parenthesis, starting a grouped expression, collection,
- /// argument or parameter list: `(`.
- LeftParen,
- /// A right round parenthesis, terminating a grouped expression, collection,
- /// argument or parameter list: `)`.
- RightParen,
- /// A comma separator in a sequence: `,`.
- Comma,
- /// A semicolon terminating an expression: `;`.
- Semicolon,
- /// A colon between name/key and value in a dictionary, argument or
- /// parameter list, or between the term and body of a term list term: `:`.
- Colon,
- /// The strong text toggle, multiplication operator, and wildcard import
- /// symbol: `*`.
- Star,
- /// Toggles emphasized text and indicates a subscript in math: `_`.
- Underscore,
- /// Starts and ends a mathematical equation: `$`.
- Dollar,
- /// The unary plus and binary addition operator: `+`.
- Plus,
- /// The unary negation and binary subtraction operator: `-`.
- Minus,
- /// The division operator and fraction operator in math: `/`.
- Slash,
- /// The superscript operator in math: `^`.
- Hat,
- /// The field access and method call operator: `.`.
- Dot,
- /// The assignment operator: `=`.
- Eq,
- /// The equality operator: `==`.
- EqEq,
- /// The inequality operator: `!=`.
- ExclEq,
- /// The less-than operator: `<`.
- Lt,
- /// The less-than or equal operator: `<=`.
- LtEq,
- /// The greater-than operator: `>`.
- Gt,
- /// The greater-than or equal operator: `>=`.
- GtEq,
- /// The add-assign operator: `+=`.
- PlusEq,
- /// The subtract-assign operator: `-=`.
- HyphEq,
- /// The multiply-assign operator: `*=`.
- StarEq,
- /// The divide-assign operator: `/=`.
- SlashEq,
- /// The spread operator: `..`.
- Dots,
- /// An arrow between a closure's parameters and body: `=>`.
- Arrow,
- /// A root: `√`, `∛` or `∜`.
- Root,
-
- /// The `not` operator.
- Not,
- /// The `and` operator.
- And,
- /// The `or` operator.
- Or,
- /// The `none` literal.
- None,
- /// The `auto` literal.
- Auto,
- /// The `let` keyword.
- Let,
- /// The `set` keyword.
- Set,
- /// The `show` keyword.
- Show,
- /// The `if` keyword.
- If,
- /// The `else` keyword.
- Else,
- /// The `for` keyword.
- For,
- /// The `in` keyword.
- In,
- /// The `while` keyword.
- While,
- /// The `break` keyword.
- Break,
- /// The `continue` keyword.
- Continue,
- /// The `return` keyword.
- Return,
- /// The `import` keyword.
- Import,
- /// The `include` keyword.
- Include,
- /// The `as` keyword.
- As,
-
- /// Code.
- Code,
- /// An identifier: `it`.
- Ident,
- /// A boolean: `true`, `false`.
- Bool,
- /// An integer: `120`.
- Int,
- /// A floating-point number: `1.2`, `10e-4`.
- Float,
- /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`.
- Numeric,
- /// A quoted string: `"..."`.
- Str,
- /// A code block: `{ let x = 1; x + 2 }`.
- CodeBlock,
- /// A content block: `[*Hi* there!]`.
- ContentBlock,
- /// A grouped expression: `(1 + 2)`.
- Parenthesized,
- /// An array: `(1, "hi", 12cm)`.
- Array,
- /// A dictionary: `(thickness: 3pt, pattern: dashed)`.
- Dict,
- /// A named pair: `thickness: 3pt`.
- Named,
- /// A keyed pair: `"spacy key": true`.
- Keyed,
- /// A unary operation: `-x`.
- Unary,
- /// A binary operation: `a + b`.
- Binary,
- /// A field access: `properties.age`.
- FieldAccess,
- /// An invocation of a function or method: `f(x, y)`.
- FuncCall,
- /// A function call's argument list: `(12pt, y)`.
- Args,
- /// Spread arguments or an argument sink: `..x`.
- Spread,
- /// A closure: `(x, y) => z`.
- Closure,
- /// A closure's parameters: `(x, y)`.
- Params,
- /// A let binding: `let x = 1`.
- LetBinding,
- /// A set rule: `set text(...)`.
- SetRule,
- /// A show rule: `show heading: it => emph(it.body)`.
- ShowRule,
- /// An if-else conditional: `if x { y } else { z }`.
- Conditional,
- /// A while loop: `while x { y }`.
- WhileLoop,
- /// A for loop: `for x in y { z }`.
- ForLoop,
- /// A module import: `import "utils.typ": a, b, c`.
- ModuleImport,
- /// Items to import from a module: `a, b, c`.
- ImportItems,
- /// A module include: `include "chapter1.typ"`.
- ModuleInclude,
- /// A break from a loop: `break`.
- LoopBreak,
- /// A continue in a loop: `continue`.
- LoopContinue,
- /// A return from a function: `return`, `return x + 1`.
- FuncReturn,
- /// A destructuring pattern: `(x, _, ..y)`.
- Destructuring,
- /// A destructuring assignment expression: `(x, y) = (1, 2)`.
- DestructAssignment,
-
- /// A line comment: `// ...`.
- LineComment,
- /// A block comment: `/* ... */`.
- BlockComment,
- /// An invalid sequence of characters.
- Error,
- /// The end of the file.
- Eof,
-}
-
-impl SyntaxKind {
- /// Is this a bracket, brace, or parenthesis?
- pub fn is_grouping(self) -> bool {
- matches!(
- self,
- Self::LeftBracket
- | Self::LeftBrace
- | Self::LeftParen
- | Self::RightBracket
- | Self::RightBrace
- | Self::RightParen
- )
- }
-
- /// Does this node terminate a preceding expression?
- pub fn is_terminator(self) -> bool {
- matches!(
- self,
- Self::Eof
- | Self::Semicolon
- | Self::RightBrace
- | Self::RightParen
- | Self::RightBracket
- )
- }
-
- /// Is this a code or content block.
- pub fn is_block(self) -> bool {
- matches!(self, Self::CodeBlock | Self::ContentBlock)
- }
-
- /// Does this node need termination through a semicolon or linebreak?
- pub fn is_stmt(self) -> bool {
- matches!(
- self,
- Self::LetBinding
- | Self::SetRule
- | Self::ShowRule
- | Self::ModuleImport
- | Self::ModuleInclude
- )
- }
-
- /// Whether this kind of node is automatically skipped by the parser in
- /// code and math mode.
- pub fn is_trivia(self) -> bool {
- matches!(
- self,
- Self::Space | Self::Parbreak | Self::LineComment | Self::BlockComment
- )
- }
-
- /// Whether this is an error.
- pub fn is_error(self) -> bool {
- self == Self::Error
- }
-
- /// A human-readable name for the kind.
- pub fn name(self) -> &'static str {
- match self {
- Self::Markup => "markup",
- Self::Text => "text",
- Self::Space => "space",
- Self::Linebreak => "line break",
- Self::Parbreak => "paragraph break",
- Self::Escape => "escape sequence",
- Self::Shorthand => "shorthand",
- Self::SmartQuote => "smart quote",
- Self::Strong => "strong content",
- Self::Emph => "emphasized content",
- Self::Raw => "raw block",
- Self::Link => "link",
- Self::Label => "label",
- Self::Ref => "reference",
- Self::RefMarker => "reference marker",
- Self::Heading => "heading",
- Self::HeadingMarker => "heading marker",
- Self::ListItem => "list item",
- Self::ListMarker => "list marker",
- Self::EnumItem => "enum item",
- Self::EnumMarker => "enum marker",
- Self::TermItem => "term list item",
- Self::TermMarker => "term marker",
- Self::Equation => "equation",
- Self::Math => "math",
- Self::MathIdent => "math identifier",
- Self::MathAlignPoint => "math alignment point",
- Self::MathDelimited => "delimited math",
- Self::MathAttach => "math attachments",
- Self::MathFrac => "math fraction",
- Self::MathRoot => "math root",
- Self::Hashtag => "hashtag",
- Self::LeftBrace => "opening brace",
- Self::RightBrace => "closing brace",
- Self::LeftBracket => "opening bracket",
- Self::RightBracket => "closing bracket",
- Self::LeftParen => "opening paren",
- Self::RightParen => "closing paren",
- Self::Comma => "comma",
- Self::Semicolon => "semicolon",
- Self::Colon => "colon",
- Self::Star => "star",
- Self::Underscore => "underscore",
- Self::Dollar => "dollar sign",
- Self::Plus => "plus",
- Self::Minus => "minus",
- Self::Slash => "slash",
- Self::Hat => "hat",
- Self::Dot => "dot",
- Self::Eq => "equals sign",
- Self::EqEq => "equality operator",
- Self::ExclEq => "inequality operator",
- Self::Lt => "less-than operator",
- Self::LtEq => "less-than or equal operator",
- Self::Gt => "greater-than operator",
- Self::GtEq => "greater-than or equal operator",
- Self::PlusEq => "add-assign operator",
- Self::HyphEq => "subtract-assign operator",
- Self::StarEq => "multiply-assign operator",
- Self::SlashEq => "divide-assign operator",
- Self::Dots => "dots",
- Self::Arrow => "arrow",
- Self::Root => "root",
- Self::Not => "operator `not`",
- Self::And => "operator `and`",
- Self::Or => "operator `or`",
- Self::None => "`none`",
- Self::Auto => "`auto`",
- Self::Let => "keyword `let`",
- Self::Set => "keyword `set`",
- Self::Show => "keyword `show`",
- Self::If => "keyword `if`",
- Self::Else => "keyword `else`",
- Self::For => "keyword `for`",
- Self::In => "keyword `in`",
- Self::While => "keyword `while`",
- Self::Break => "keyword `break`",
- Self::Continue => "keyword `continue`",
- Self::Return => "keyword `return`",
- Self::Import => "keyword `import`",
- Self::Include => "keyword `include`",
- Self::As => "keyword `as`",
- Self::Code => "code",
- Self::Ident => "identifier",
- Self::Bool => "boolean",
- Self::Int => "integer",
- Self::Float => "float",
- Self::Numeric => "numeric value",
- Self::Str => "string",
- Self::CodeBlock => "code block",
- Self::ContentBlock => "content block",
- Self::Parenthesized => "group",
- Self::Array => "array",
- Self::Dict => "dictionary",
- Self::Named => "named pair",
- Self::Keyed => "keyed pair",
- Self::Unary => "unary expression",
- Self::Binary => "binary expression",
- Self::FieldAccess => "field access",
- Self::FuncCall => "function call",
- Self::Args => "call arguments",
- Self::Spread => "spread",
- Self::Closure => "closure",
- Self::Params => "closure parameters",
- Self::LetBinding => "`let` expression",
- Self::SetRule => "`set` expression",
- Self::ShowRule => "`show` expression",
- Self::Conditional => "`if` expression",
- Self::WhileLoop => "while-loop expression",
- Self::ForLoop => "for-loop expression",
- Self::ModuleImport => "`import` expression",
- Self::ImportItems => "import items",
- Self::ModuleInclude => "`include` expression",
- Self::LoopBreak => "`break` expression",
- Self::LoopContinue => "`continue` expression",
- Self::FuncReturn => "`return` expression",
- Self::Destructuring => "destructuring pattern",
- Self::DestructAssignment => "destructuring assignment expression",
- Self::LineComment => "line comment",
- Self::BlockComment => "block comment",
- Self::Error => "syntax error",
- Self::Eof => "end of file",
- }
- }
-}
diff --git a/src/syntax/lexer.rs b/src/syntax/lexer.rs
deleted file mode 100644
index d95b5b7b..00000000
--- a/src/syntax/lexer.rs
+++ /dev/null
@@ -1,738 +0,0 @@
-use ecow::{eco_format, EcoString};
-use unicode_ident::{is_xid_continue, is_xid_start};
-use unicode_segmentation::UnicodeSegmentation;
-use unscanny::Scanner;
-
-use super::SyntaxKind;
-
-/// Splits up a string of source code into tokens.
-#[derive(Clone)]
-pub(super) struct Lexer<'s> {
- /// The underlying scanner.
- s: Scanner<'s>,
- /// The mode the lexer is in. This determines which kinds of tokens it
- /// produces.
- mode: LexMode,
- /// Whether the last token contained a newline.
- newline: bool,
- /// An error for the last token.
- error: Option<EcoString>,
-}
-
-/// What kind of tokens to emit.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub(super) enum LexMode {
- /// Text and markup.
- Markup,
- /// Math atoms, operators, etc.
- Math,
- /// Keywords, literals and operators.
- Code,
-}
-
-impl<'s> Lexer<'s> {
- /// Create a new lexer with the given mode and a prefix to offset column
- /// calculations.
- pub fn new(text: &'s str, mode: LexMode) -> Self {
- Self {
- s: Scanner::new(text),
- mode,
- newline: false,
- error: None,
- }
- }
-
- /// Get the current lexing mode.
- pub fn mode(&self) -> LexMode {
- self.mode
- }
-
- /// Change the lexing mode.
- pub fn set_mode(&mut self, mode: LexMode) {
- self.mode = mode;
- }
-
- /// The index in the string at which the last token ends and next token
- /// will start.
- pub fn cursor(&self) -> usize {
- self.s.cursor()
- }
-
- /// Jump to the given index in the string.
- pub fn jump(&mut self, index: usize) {
- self.s.jump(index);
- }
-
- /// Whether the last token contained a newline.
- pub fn newline(&self) -> bool {
- self.newline
- }
-
- /// Take out the last error, if any.
- pub fn take_error(&mut self) -> Option<EcoString> {
- self.error.take()
- }
-}
-
-impl Lexer<'_> {
- /// Construct a full-positioned syntax error.
- fn error(&mut self, message: impl Into<EcoString>) -> SyntaxKind {
- self.error = Some(message.into());
- SyntaxKind::Error
- }
-}
-
-/// Shared.
-impl Lexer<'_> {
- pub fn next(&mut self) -> SyntaxKind {
- self.newline = false;
- self.error = None;
- let start = self.s.cursor();
- match self.s.eat() {
- Some(c) if c.is_whitespace() => self.whitespace(start, c),
- Some('/') if self.s.eat_if('/') => self.line_comment(),
- Some('/') if self.s.eat_if('*') => self.block_comment(),
- Some('*') if self.s.eat_if('/') => {
- self.error("unexpected end of block comment")
- }
-
- Some(c) => match self.mode {
- LexMode::Markup => self.markup(start, c),
- LexMode::Math => self.math(start, c),
- LexMode::Code => self.code(start, c),
- },
-
- None => SyntaxKind::Eof,
- }
- }
-
- fn whitespace(&mut self, start: usize, c: char) -> SyntaxKind {
- let more = self.s.eat_while(char::is_whitespace);
- let newlines = match c {
- ' ' if more.is_empty() => 0,
- _ => count_newlines(self.s.from(start)),
- };
-
- self.newline = newlines > 0;
- if self.mode == LexMode::Markup && newlines >= 2 {
- SyntaxKind::Parbreak
- } else {
- SyntaxKind::Space
- }
- }
-
- fn line_comment(&mut self) -> SyntaxKind {
- self.s.eat_until(is_newline);
- SyntaxKind::LineComment
- }
-
- fn block_comment(&mut self) -> SyntaxKind {
- let mut state = '_';
- let mut depth = 1;
-
- // Find the first `*/` that does not correspond to a nested `/*`.
- while let Some(c) = self.s.eat() {
- state = match (state, c) {
- ('*', '/') => {
- depth -= 1;
- if depth == 0 {
- break;
- }
- '_'
- }
- ('/', '*') => {
- depth += 1;
- '_'
- }
- ('/', '/') => {
- self.line_comment();
- '_'
- }
- _ => c,
- }
- }
-
- SyntaxKind::BlockComment
- }
-}
-
-/// Markup.
-impl Lexer<'_> {
- fn markup(&mut self, start: usize, c: char) -> SyntaxKind {
- match c {
- '\\' => self.backslash(),
- '`' => self.raw(),
- 'h' if self.s.eat_if("ttp://") => self.link(),
- 'h' if self.s.eat_if("ttps://") => self.link(),
- '<' if self.s.at(is_id_continue) => self.label(),
- '@' => self.ref_marker(),
-
- '.' if self.s.eat_if("..") => SyntaxKind::Shorthand,
- '-' if self.s.eat_if("--") => SyntaxKind::Shorthand,
- '-' if self.s.eat_if('-') => SyntaxKind::Shorthand,
- '-' if self.s.eat_if('?') => SyntaxKind::Shorthand,
- '*' if !self.in_word() => SyntaxKind::Star,
- '_' if !self.in_word() => SyntaxKind::Underscore,
-
- '#' => SyntaxKind::Hashtag,
- '[' => SyntaxKind::LeftBracket,
- ']' => SyntaxKind::RightBracket,
- '\'' => SyntaxKind::SmartQuote,
- '"' => SyntaxKind::SmartQuote,
- '$' => SyntaxKind::Dollar,
- '~' => SyntaxKind::Shorthand,
- ':' => SyntaxKind::Colon,
- '=' => {
- self.s.eat_while('=');
- if self.space_or_end() {
- SyntaxKind::HeadingMarker
- } else {
- self.text()
- }
- }
- '-' if self.space_or_end() => SyntaxKind::ListMarker,
- '+' if self.space_or_end() => SyntaxKind::EnumMarker,
- '/' if self.space_or_end() => SyntaxKind::TermMarker,
- '0'..='9' => self.numbering(start),
-
- _ => self.text(),
- }
- }
-
- fn backslash(&mut self) -> SyntaxKind {
- if self.s.eat_if("u{") {
- let hex = self.s.eat_while(char::is_ascii_alphanumeric);
- if !self.s.eat_if('}') {
- return self.error("unclosed Unicode escape sequence");
- }
-
- if u32::from_str_radix(hex, 16)
- .ok()
- .and_then(std::char::from_u32)
- .is_none()
- {
- return self.error(eco_format!("invalid Unicode codepoint: {}", hex));
- }
-
- return SyntaxKind::Escape;
- }
-
- if self.s.done() || self.s.at(char::is_whitespace) {
- SyntaxKind::Linebreak
- } else {
- self.s.eat();
- SyntaxKind::Escape
- }
- }
-
- fn raw(&mut self) -> SyntaxKind {
- let mut backticks = 1;
- while self.s.eat_if('`') {
- backticks += 1;
- }
-
- if backticks == 2 {
- return SyntaxKind::Raw;
- }
-
- let mut found = 0;
- while found < backticks {
- match self.s.eat() {
- Some('`') => found += 1,
- Some(_) => found = 0,
- None => break,
- }
- }
-
- if found != backticks {
- return self.error("unclosed raw text");
- }
-
- SyntaxKind::Raw
- }
-
- fn link(&mut self) -> SyntaxKind {
- let mut brackets = Vec::new();
-
- #[rustfmt::skip]
- self.s.eat_while(|c: char| {
- match c {
- | '0' ..= '9'
- | 'a' ..= 'z'
- | 'A' ..= 'Z'
- | '!' | '#' | '$' | '%' | '&' | '*' | '+'
- | ',' | '-' | '.' | '/' | ':' | ';' | '='
- | '?' | '@' | '_' | '~' | '\'' => true,
- '[' => {
- brackets.push(SyntaxKind::LeftBracket);
- true
- }
- '(' => {
- brackets.push(SyntaxKind::LeftParen);
- true
- }
- ']' => brackets.pop() == Some(SyntaxKind::LeftBracket),
- ')' => brackets.pop() == Some(SyntaxKind::LeftParen),
- _ => false,
- }
- });
-
- if !brackets.is_empty() {
- return self.error(
- "automatic links cannot contain unbalanced brackets, \
- use the `link` function instead",
- );
- }
-
- // Don't include the trailing characters likely to be part of text.
- while matches!(self.s.scout(-1), Some('!' | ',' | '.' | ':' | ';' | '?' | '\'')) {
- self.s.uneat();
- }
-
- SyntaxKind::Link
- }
-
- fn numbering(&mut self, start: usize) -> SyntaxKind {
- self.s.eat_while(char::is_ascii_digit);
-
- let read = self.s.from(start);
- if self.s.eat_if('.') && self.space_or_end() && read.parse::<usize>().is_ok() {
- return SyntaxKind::EnumMarker;
- }
-
- self.text()
- }
-
- fn ref_marker(&mut self) -> SyntaxKind {
- self.s.eat_while(|c| is_id_continue(c) || matches!(c, ':' | '.'));
-
- // Don't include the trailing characters likely to be part of text.
- while matches!(self.s.scout(-1), Some('.' | ':')) {
- self.s.uneat();
- }
-
- SyntaxKind::RefMarker
- }
-
- fn label(&mut self) -> SyntaxKind {
- let label = self.s.eat_while(|c| is_id_continue(c) || matches!(c, ':' | '.'));
- if label.is_empty() {
- return self.error("label cannot be empty");
- }
-
- if !self.s.eat_if('>') {
- return self.error("unclosed label");
- }
-
- SyntaxKind::Label
- }
-
- fn text(&mut self) -> SyntaxKind {
- macro_rules! table {
- ($(|$c:literal)*) => {
- static TABLE: [bool; 128] = {
- let mut t = [false; 128];
- $(t[$c as usize] = true;)*
- t
- };
- };
- }
-
- table! {
- | ' ' | '\t' | '\n' | '\x0b' | '\x0c' | '\r' | '\\' | '/'
- | '[' | ']' | '{' | '}' | '~' | '-' | '.' | '\'' | '"'
- | '*' | '_' | ':' | 'h' | '`' | '$' | '<' | '>' | '@' | '#'
- };
-
- loop {
- self.s.eat_until(|c: char| {
- TABLE.get(c as usize).copied().unwrap_or_else(|| c.is_whitespace())
- });
-
- // Continue with the same text node if the thing would become text
- // anyway.
- let mut s = self.s;
- match s.eat() {
- Some(' ') if s.at(char::is_alphanumeric) => {}
- Some('/') if !s.at(['/', '*']) => {}
- Some('-') if !s.at(['-', '?']) => {}
- Some('.') if !s.at("..") => {}
- Some('h') if !s.at("ttp://") && !s.at("ttps://") => {}
- Some('@') if !s.at(is_id_start) => {}
- _ => break,
- }
-
- self.s = s;
- }
-
- SyntaxKind::Text
- }
-
- fn in_word(&self) -> bool {
- let alphanum = |c: Option<char>| c.map_or(false, |c| c.is_alphanumeric());
- let prev = self.s.scout(-2);
- let next = self.s.peek();
- alphanum(prev) && alphanum(next)
- }
-
- fn space_or_end(&self) -> bool {
- self.s.done() || self.s.at(char::is_whitespace)
- }
-}
-
-/// Math.
-impl Lexer<'_> {
- fn math(&mut self, start: usize, c: char) -> SyntaxKind {
- match c {
- '\\' => self.backslash(),
- '"' => self.string(),
-
- '-' if self.s.eat_if(">>") => SyntaxKind::Shorthand,
- '-' if self.s.eat_if('>') => SyntaxKind::Shorthand,
- '-' if self.s.eat_if("->") => SyntaxKind::Shorthand,
- ':' if self.s.eat_if('=') => SyntaxKind::Shorthand,
- ':' if self.s.eat_if(":=") => SyntaxKind::Shorthand,
- '!' if self.s.eat_if('=') => SyntaxKind::Shorthand,
- '.' if self.s.eat_if("..") => SyntaxKind::Shorthand,
- '[' if self.s.eat_if('|') => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("==>") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("-->") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("--") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("-<") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("->") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("<-") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("<<") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("==") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if("~~") => SyntaxKind::Shorthand,
- '<' if self.s.eat_if('=') => SyntaxKind::Shorthand,
- '<' if self.s.eat_if('<') => SyntaxKind::Shorthand,
- '<' if self.s.eat_if('-') => SyntaxKind::Shorthand,
- '<' if self.s.eat_if('~') => SyntaxKind::Shorthand,
- '>' if self.s.eat_if("->") => SyntaxKind::Shorthand,
- '>' if self.s.eat_if(">>") => SyntaxKind::Shorthand,
- '=' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
- '=' if self.s.eat_if('>') => SyntaxKind::Shorthand,
- '=' if self.s.eat_if(':') => SyntaxKind::Shorthand,
- '>' if self.s.eat_if('=') => SyntaxKind::Shorthand,
- '>' if self.s.eat_if('>') => SyntaxKind::Shorthand,
- '|' if self.s.eat_if("->") => SyntaxKind::Shorthand,
- '|' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
- '|' if self.s.eat_if(']') => SyntaxKind::Shorthand,
- '|' if self.s.eat_if('|') => SyntaxKind::Shorthand,
- '~' if self.s.eat_if("~>") => SyntaxKind::Shorthand,
- '~' if self.s.eat_if('>') => SyntaxKind::Shorthand,
- '*' | '\'' | '-' => SyntaxKind::Shorthand,
-
- '#' => SyntaxKind::Hashtag,
- '_' => SyntaxKind::Underscore,
- '$' => SyntaxKind::Dollar,
- '/' => SyntaxKind::Slash,
- '^' => SyntaxKind::Hat,
- '&' => SyntaxKind::MathAlignPoint,
- '√' | '∛' | '∜' => SyntaxKind::Root,
-
- // Identifiers.
- c if is_math_id_start(c) && self.s.at(is_math_id_continue) => {
- self.s.eat_while(is_math_id_continue);
- SyntaxKind::MathIdent
- }
-
- // Other math atoms.
- _ => self.math_text(start, c),
- }
- }
-
- fn math_text(&mut self, start: usize, c: char) -> SyntaxKind {
- // Keep numbers and grapheme clusters together.
- if c.is_numeric() {
- self.s.eat_while(char::is_numeric);
- let mut s = self.s;
- if s.eat_if('.') && !s.eat_while(char::is_numeric).is_empty() {
- self.s = s;
- }
- } else {
- let len = self
- .s
- .get(start..self.s.string().len())
- .graphemes(true)
- .next()
- .map_or(0, str::len);
- self.s.jump(start + len);
- }
- SyntaxKind::Text
- }
-}
-
-/// Code.
-impl Lexer<'_> {
- fn code(&mut self, start: usize, c: char) -> SyntaxKind {
- match c {
- '`' => self.raw(),
- '<' if self.s.at(is_id_continue) => self.label(),
- '0'..='9' => self.number(start, c),
- '.' if self.s.at(char::is_ascii_digit) => self.number(start, c),
- '"' => self.string(),
-
- '=' if self.s.eat_if('=') => SyntaxKind::EqEq,
- '!' if self.s.eat_if('=') => SyntaxKind::ExclEq,
- '<' if self.s.eat_if('=') => SyntaxKind::LtEq,
- '>' if self.s.eat_if('=') => SyntaxKind::GtEq,
- '+' if self.s.eat_if('=') => SyntaxKind::PlusEq,
- '-' if self.s.eat_if('=') => SyntaxKind::HyphEq,
- '*' if self.s.eat_if('=') => SyntaxKind::StarEq,
- '/' if self.s.eat_if('=') => SyntaxKind::SlashEq,
- '.' if self.s.eat_if('.') => SyntaxKind::Dots,
- '=' if self.s.eat_if('>') => SyntaxKind::Arrow,
-
- '{' => SyntaxKind::LeftBrace,
- '}' => SyntaxKind::RightBrace,
- '[' => SyntaxKind::LeftBracket,
- ']' => SyntaxKind::RightBracket,
- '(' => SyntaxKind::LeftParen,
- ')' => SyntaxKind::RightParen,
- '$' => SyntaxKind::Dollar,
- ',' => SyntaxKind::Comma,
- ';' => SyntaxKind::Semicolon,
- ':' => SyntaxKind::Colon,
- '.' => SyntaxKind::Dot,
- '+' => SyntaxKind::Plus,
- '-' => SyntaxKind::Minus,
- '*' => SyntaxKind::Star,
- '/' => SyntaxKind::Slash,
- '=' => SyntaxKind::Eq,
- '<' => SyntaxKind::Lt,
- '>' => SyntaxKind::Gt,
-
- c if is_id_start(c) => self.ident(start),
-
- c => self.error(eco_format!("the character `{c}` is not valid in code")),
- }
- }
-
- fn ident(&mut self, start: usize) -> SyntaxKind {
- self.s.eat_while(is_id_continue);
- let ident = self.s.from(start);
-
- let prev = self.s.get(0..start);
- if !prev.ends_with(['.', '@']) || prev.ends_with("..") {
- if let Some(keyword) = keyword(ident) {
- return keyword;
- }
- }
-
- if ident == "_" {
- SyntaxKind::Underscore
- } else {
- SyntaxKind::Ident
- }
- }
-
- fn number(&mut self, mut start: usize, c: char) -> SyntaxKind {
- // Handle alternative integer bases.
- let mut base = 10;
- if c == '0' {
- if self.s.eat_if('b') {
- base = 2;
- } else if self.s.eat_if('o') {
- base = 8;
- } else if self.s.eat_if('x') {
- base = 16;
- }
- if base != 10 {
- start = self.s.cursor();
- }
- }
-
- // Read the first part (integer or fractional depending on `first`).
- self.s.eat_while(if base == 16 {
- char::is_ascii_alphanumeric
- } else {
- char::is_ascii_digit
- });
-
- // Read the fractional part if not already done.
- // Make sure not to confuse a range for the decimal separator.
- if c != '.'
- && !self.s.at("..")
- && !self.s.scout(1).map_or(false, is_id_start)
- && self.s.eat_if('.')
- && base == 10
- {
- self.s.eat_while(char::is_ascii_digit);
- }
-
- // Read the exponent.
- if !self.s.at("em") && self.s.eat_if(['e', 'E']) && base == 10 {
- self.s.eat_if(['+', '-']);
- self.s.eat_while(char::is_ascii_digit);
- }
-
- // Read the suffix.
- let suffix_start = self.s.cursor();
- if !self.s.eat_if('%') {
- self.s.eat_while(char::is_ascii_alphanumeric);
- }
-
- let number = self.s.get(start..suffix_start);
- let suffix = self.s.from(suffix_start);
-
- let kind = if i64::from_str_radix(number, base).is_ok() {
- SyntaxKind::Int
- } else if base == 10 && number.parse::<f64>().is_ok() {
- SyntaxKind::Float
- } else {
- return self.error(match base {
- 2 => eco_format!("invalid binary number: 0b{}", number),
- 8 => eco_format!("invalid octal number: 0o{}", number),
- 16 => eco_format!("invalid hexadecimal number: 0x{}", number),
- _ => eco_format!("invalid number: {}", number),
- });
- };
-
- if suffix.is_empty() {
- return kind;
- }
-
- if !matches!(
- suffix,
- "pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%"
- ) {
- return self.error(eco_format!("invalid number suffix: {}", suffix));
- }
-
- SyntaxKind::Numeric
- }
-
- fn string(&mut self) -> SyntaxKind {
- let mut escaped = false;
- self.s.eat_until(|c| {
- let stop = c == '"' && !escaped;
- escaped = c == '\\' && !escaped;
- stop
- });
-
- if !self.s.eat_if('"') {
- return self.error("unclosed string");
- }
-
- SyntaxKind::Str
- }
-}
-
-/// Try to parse an identifier into a keyword.
-fn keyword(ident: &str) -> Option<SyntaxKind> {
- Some(match ident {
- "none" => SyntaxKind::None,
- "auto" => SyntaxKind::Auto,
- "true" => SyntaxKind::Bool,
- "false" => SyntaxKind::Bool,
- "not" => SyntaxKind::Not,
- "and" => SyntaxKind::And,
- "or" => SyntaxKind::Or,
- "let" => SyntaxKind::Let,
- "set" => SyntaxKind::Set,
- "show" => SyntaxKind::Show,
- "if" => SyntaxKind::If,
- "else" => SyntaxKind::Else,
- "for" => SyntaxKind::For,
- "in" => SyntaxKind::In,
- "while" => SyntaxKind::While,
- "break" => SyntaxKind::Break,
- "continue" => SyntaxKind::Continue,
- "return" => SyntaxKind::Return,
- "import" => SyntaxKind::Import,
- "include" => SyntaxKind::Include,
- "as" => SyntaxKind::As,
- _ => return None,
- })
-}
-
-/// Whether this character denotes a newline.
-#[inline]
-pub fn is_newline(character: char) -> bool {
- matches!(
- character,
- // Line Feed, Vertical Tab, Form Feed, Carriage Return.
- '\n' | '\x0B' | '\x0C' | '\r' |
- // Next Line, Line Separator, Paragraph Separator.
- '\u{0085}' | '\u{2028}' | '\u{2029}'
- )
-}
-
-/// Split text at newlines.
-pub(super) fn split_newlines(text: &str) -> Vec<&str> {
- let mut s = Scanner::new(text);
- let mut lines = Vec::new();
- let mut start = 0;
- let mut end = 0;
-
- while let Some(c) = s.eat() {
- if is_newline(c) {
- if c == '\r' {
- s.eat_if('\n');
- }
-
- lines.push(&text[start..end]);
- start = s.cursor();
- }
- end = s.cursor();
- }
-
- lines.push(&text[start..]);
- lines
-}
-
-/// Count the number of newlines in text.
-fn count_newlines(text: &str) -> usize {
- let mut newlines = 0;
- let mut s = Scanner::new(text);
- while let Some(c) = s.eat() {
- if is_newline(c) {
- if c == '\r' {
- s.eat_if('\n');
- }
- newlines += 1;
- }
- }
- newlines
-}
-
-/// Whether a string is a valid Typst identifier.
-///
-/// In addition to what is specified in the [Unicode Standard][uax31], we allow:
-/// - `_` as a starting character,
-/// - `_` and `-` as continuing characters.
-///
-/// [uax31]: http://www.unicode.org/reports/tr31/
-#[inline]
-pub fn is_ident(string: &str) -> bool {
- let mut chars = string.chars();
- chars
- .next()
- .map_or(false, |c| is_id_start(c) && chars.all(is_id_continue))
-}
-
-/// Whether a character can start an identifier.
-#[inline]
-pub(crate) fn is_id_start(c: char) -> bool {
- is_xid_start(c) || c == '_'
-}
-
-/// Whether a character can continue an identifier.
-#[inline]
-pub(crate) fn is_id_continue(c: char) -> bool {
- is_xid_continue(c) || c == '_' || c == '-'
-}
-
-/// Whether a character can start an identifier in math.
-#[inline]
-fn is_math_id_start(c: char) -> bool {
- is_xid_start(c)
-}
-
-/// Whether a character can continue an identifier in math.
-#[inline]
-fn is_math_id_continue(c: char) -> bool {
- is_xid_continue(c) && c != '_'
-}
diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs
deleted file mode 100644
index 1ce1e4c0..00000000
--- a/src/syntax/mod.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-//! Syntax definition, parsing, and highlighting.
-
-pub mod ast;
-
-mod kind;
-mod lexer;
-mod node;
-mod parser;
-mod reparser;
-mod source;
-mod span;
-
-pub use self::kind::SyntaxKind;
-pub use self::lexer::{is_ident, is_newline};
-pub use self::node::{LinkedChildren, LinkedNode, SyntaxNode};
-pub use self::parser::{parse, parse_code};
-pub use self::source::Source;
-pub use self::span::{Span, Spanned};
-
-pub(crate) use self::lexer::{is_id_continue, is_id_start};
-
-use self::lexer::{split_newlines, LexMode, Lexer};
-use self::parser::{reparse_block, reparse_markup};
diff --git a/src/syntax/node.rs b/src/syntax/node.rs
deleted file mode 100644
index 6a66416d..00000000
--- a/src/syntax/node.rs
+++ /dev/null
@@ -1,889 +0,0 @@
-use std::fmt::{self, Debug, Display, Formatter};
-use std::ops::{Deref, Range};
-use std::rc::Rc;
-use std::sync::Arc;
-
-use ecow::EcoString;
-
-use super::ast::AstNode;
-use super::{Span, SyntaxKind};
-use crate::diag::SourceError;
-use crate::file::FileId;
-
-/// A node in the untyped syntax tree.
-#[derive(Clone, Eq, PartialEq, Hash)]
-pub struct SyntaxNode(Repr);
-
-/// The three internal representations.
-#[derive(Clone, Eq, PartialEq, Hash)]
-enum Repr {
- /// A leaf node.
- Leaf(LeafNode),
- /// A reference-counted inner node.
- Inner(Arc<InnerNode>),
- /// An error node.
- Error(Arc<ErrorNode>),
-}
-
-impl SyntaxNode {
- /// Create a new leaf node.
- pub fn leaf(kind: SyntaxKind, text: impl Into<EcoString>) -> Self {
- Self(Repr::Leaf(LeafNode::new(kind, text)))
- }
-
- /// Create a new inner node with children.
- pub fn inner(kind: SyntaxKind, children: Vec<SyntaxNode>) -> Self {
- Self(Repr::Inner(Arc::new(InnerNode::new(kind, children))))
- }
-
- /// Create a new error node.
- pub fn error(message: impl Into<EcoString>, text: impl Into<EcoString>) -> Self {
- Self(Repr::Error(Arc::new(ErrorNode::new(message, text))))
- }
-
- /// The type of the node.
- pub fn kind(&self) -> SyntaxKind {
- match &self.0 {
- Repr::Leaf(leaf) => leaf.kind,
- Repr::Inner(inner) => inner.kind,
- Repr::Error(_) => SyntaxKind::Error,
- }
- }
-
- /// Return `true` if the length is 0.
- pub fn is_empty(&self) -> bool {
- self.len() == 0
- }
-
- /// The byte length of the node in the source text.
- pub fn len(&self) -> usize {
- match &self.0 {
- Repr::Leaf(leaf) => leaf.len(),
- Repr::Inner(inner) => inner.len,
- Repr::Error(error) => error.len(),
- }
- }
-
- /// The span of the node.
- pub fn span(&self) -> Span {
- match &self.0 {
- Repr::Leaf(leaf) => leaf.span,
- Repr::Inner(inner) => inner.span,
- Repr::Error(error) => error.span,
- }
- }
-
- /// The text of the node if it is a leaf node.
- ///
- /// Returns the empty string if this is an inner node.
- pub fn text(&self) -> &EcoString {
- static EMPTY: EcoString = EcoString::new();
- match &self.0 {
- Repr::Leaf(leaf) => &leaf.text,
- Repr::Error(error) => &error.text,
- Repr::Inner(_) => &EMPTY,
- }
- }
-
- /// Extract the text from the node.
- ///
- /// Builds the string if this is an inner node.
- pub fn into_text(self) -> EcoString {
- match self.0 {
- Repr::Leaf(leaf) => leaf.text,
- Repr::Error(error) => error.text.clone(),
- Repr::Inner(node) => {
- node.children.iter().cloned().map(Self::into_text).collect()
- }
- }
- }
-
- /// The node's children.
- pub fn children(&self) -> std::slice::Iter<'_, SyntaxNode> {
- match &self.0 {
- Repr::Leaf(_) | Repr::Error(_) => [].iter(),
- Repr::Inner(inner) => inner.children.iter(),
- }
- }
-
- /// Whether the node can be cast to the given AST node.
- pub fn is<T: AstNode>(&self) -> bool {
- self.cast::<T>().is_some()
- }
-
- /// Try to convert the node to a typed AST node.
- pub fn cast<T: AstNode>(&self) -> Option<T> {
- T::from_untyped(self)
- }
-
- /// Cast the first child that can cast to the AST type `T`.
- pub fn cast_first_match<T: AstNode>(&self) -> Option<T> {
- self.children().find_map(Self::cast)
- }
-
- /// Cast the last child that can cast to the AST type `T`.
- pub fn cast_last_match<T: AstNode>(&self) -> Option<T> {
- self.children().rev().find_map(Self::cast)
- }
-
- /// Whether the node or its children contain an error.
- pub fn erroneous(&self) -> bool {
- match &self.0 {
- Repr::Leaf(_) => false,
- Repr::Inner(node) => node.erroneous,
- Repr::Error(_) => true,
- }
- }
-
- /// The error messages for this node and its descendants.
- pub fn errors(&self) -> Vec<SourceError> {
- if !self.erroneous() {
- return vec![];
- }
-
- if let Repr::Error(error) = &self.0 {
- vec![SourceError::new(error.span, error.message.clone())]
- } else {
- self.children()
- .filter(|node| node.erroneous())
- .flat_map(|node| node.errors())
- .collect()
- }
- }
-
- /// Set a synthetic span for the node and all its descendants.
- pub fn synthesize(&mut self, span: Span) {
- match &mut self.0 {
- Repr::Leaf(leaf) => leaf.span = span,
- Repr::Inner(inner) => Arc::make_mut(inner).synthesize(span),
- Repr::Error(error) => Arc::make_mut(error).span = span,
- }
- }
-}
-
-impl SyntaxNode {
- /// Mark this node as erroneous.
- pub(super) fn make_erroneous(&mut self) {
- if let Repr::Inner(inner) = &mut self.0 {
- Arc::make_mut(inner).erroneous = true;
- }
- }
-
- /// Convert the child to another kind.
- #[track_caller]
- pub(super) fn convert_to_kind(&mut self, kind: SyntaxKind) {
- debug_assert!(!kind.is_error());
- match &mut self.0 {
- Repr::Leaf(leaf) => leaf.kind = kind,
- Repr::Inner(inner) => Arc::make_mut(inner).kind = kind,
- Repr::Error(_) => panic!("cannot convert error"),
- }
- }
-
- /// Convert the child to an error.
- pub(super) fn convert_to_error(&mut self, message: impl Into<EcoString>) {
- let text = std::mem::take(self).into_text();
- *self = SyntaxNode::error(message, text);
- }
-
- /// Assign spans to each node.
- #[tracing::instrument(skip_all)]
- pub(super) fn numberize(
- &mut self,
- id: FileId,
- within: Range<u64>,
- ) -> NumberingResult {
- if within.start >= within.end {
- return Err(Unnumberable);
- }
-
- let mid = Span::new(id, (within.start + within.end) / 2);
- match &mut self.0 {
- Repr::Leaf(leaf) => leaf.span = mid,
- Repr::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within)?,
- Repr::Error(error) => Arc::make_mut(error).span = mid,
- }
-
- Ok(())
- }
-
- /// Whether this is a leaf node.
- pub(super) fn is_leaf(&self) -> bool {
- matches!(self.0, Repr::Leaf(_))
- }
-
- /// The number of descendants, including the node itself.
- pub(super) fn descendants(&self) -> usize {
- match &self.0 {
- Repr::Leaf(_) | Repr::Error(_) => 1,
- Repr::Inner(inner) => inner.descendants,
- }
- }
-
- /// The node's children, mutably.
- pub(super) fn children_mut(&mut self) -> &mut [SyntaxNode] {
- match &mut self.0 {
- Repr::Leaf(_) | Repr::Error(_) => &mut [],
- Repr::Inner(inner) => &mut Arc::make_mut(inner).children,
- }
- }
-
- /// Replaces a range of children with a replacement.
- ///
- /// May have mutated the children if it returns `Err(_)`.
- pub(super) fn replace_children(
- &mut self,
- range: Range<usize>,
- replacement: Vec<SyntaxNode>,
- ) -> NumberingResult {
- if let Repr::Inner(inner) = &mut self.0 {
- Arc::make_mut(inner).replace_children(range, replacement)?;
- }
- Ok(())
- }
-
- /// Update this node after changes were made to one of its children.
- pub(super) fn update_parent(
- &mut self,
- prev_len: usize,
- new_len: usize,
- prev_descendants: usize,
- new_descendants: usize,
- ) {
- if let Repr::Inner(inner) = &mut self.0 {
- Arc::make_mut(inner).update_parent(
- prev_len,
- new_len,
- prev_descendants,
- new_descendants,
- );
- }
- }
-
- /// The upper bound of assigned numbers in this subtree.
- pub(super) fn upper(&self) -> u64 {
- match &self.0 {
- Repr::Inner(inner) => inner.upper,
- Repr::Leaf(leaf) => leaf.span.number() + 1,
- Repr::Error(error) => error.span.number() + 1,
- }
- }
-}
-
-impl Debug for SyntaxNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match &self.0 {
- Repr::Inner(node) => node.fmt(f),
- Repr::Leaf(node) => node.fmt(f),
- Repr::Error(node) => node.fmt(f),
- }
- }
-}
-
-impl Default for SyntaxNode {
- fn default() -> Self {
- Self::error("", "")
- }
-}
-
-/// A leaf node in the untyped syntax tree.
-#[derive(Clone, Eq, PartialEq, Hash)]
-struct LeafNode {
- /// What kind of node this is (each kind would have its own struct in a
- /// strongly typed AST).
- kind: SyntaxKind,
- /// The source text of the node.
- text: EcoString,
- /// The node's span.
- span: Span,
-}
-
-impl LeafNode {
- /// Create a new leaf node.
- #[track_caller]
- fn new(kind: SyntaxKind, text: impl Into<EcoString>) -> Self {
- debug_assert!(!kind.is_error());
- Self { kind, text: text.into(), span: Span::detached() }
- }
-
- /// The byte length of the node in the source text.
- fn len(&self) -> usize {
- self.text.len()
- }
-}
-
-impl Debug for LeafNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "{:?}: {:?}", self.kind, self.text)
- }
-}
-
-/// An inner node in the untyped syntax tree.
-#[derive(Clone, Eq, PartialEq, Hash)]
-struct InnerNode {
- /// What kind of node this is (each kind would have its own struct in a
- /// strongly typed AST).
- kind: SyntaxKind,
- /// The byte length of the node in the source.
- len: usize,
- /// The node's span.
- span: Span,
- /// The number of nodes in the whole subtree, including this node.
- descendants: usize,
- /// Whether this node or any of its children are erroneous.
- erroneous: bool,
- /// The upper bound of this node's numbering range.
- upper: u64,
- /// This node's children, losslessly make up this node.
- children: Vec<SyntaxNode>,
-}
-
-impl InnerNode {
- /// Create a new inner node with the given kind and children.
- #[track_caller]
- fn new(kind: SyntaxKind, children: Vec<SyntaxNode>) -> Self {
- debug_assert!(!kind.is_error());
-
- let mut len = 0;
- let mut descendants = 1;
- let mut erroneous = false;
-
- for child in &children {
- len += child.len();
- descendants += child.descendants();
- erroneous |= child.erroneous();
- }
-
- Self {
- kind,
- len,
- span: Span::detached(),
- descendants,
- erroneous,
- upper: 0,
- children,
- }
- }
-
- /// Set a synthetic span for the node and all its descendants.
- fn synthesize(&mut self, span: Span) {
- self.span = span;
- self.upper = span.number();
- for child in &mut self.children {
- child.synthesize(span);
- }
- }
-
- /// Assign span numbers `within` an interval to this node's subtree or just
- /// a `range` of its children.
- fn numberize(
- &mut self,
- id: FileId,
- range: Option<Range<usize>>,
- within: Range<u64>,
- ) -> NumberingResult {
- // Determine how many nodes we will number.
- let descendants = match &range {
- Some(range) if range.is_empty() => return Ok(()),
- Some(range) => self.children[range.clone()]
- .iter()
- .map(SyntaxNode::descendants)
- .sum::<usize>(),
- None => self.descendants,
- };
-
- // Determine the distance between two neighbouring assigned numbers. If
- // possible, we try to fit all numbers into the left half of `within`
- // so that there is space for future insertions.
- let space = within.end - within.start;
- let mut stride = space / (2 * descendants as u64);
- if stride == 0 {
- stride = space / self.descendants as u64;
- if stride == 0 {
- return Err(Unnumberable);
- }
- }
-
- // Number the node itself.
- let mut start = within.start;
- if range.is_none() {
- let end = start + stride;
- self.span = Span::new(id, (start + end) / 2);
- self.upper = within.end;
- start = end;
- }
-
- // Number the children.
- let len = self.children.len();
- for child in &mut self.children[range.unwrap_or(0..len)] {
- let end = start + child.descendants() as u64 * stride;
- child.numberize(id, start..end)?;
- start = end;
- }
-
- Ok(())
- }
-
- /// Replaces a range of children with a replacement.
- ///
- /// May have mutated the children if it returns `Err(_)`.
- fn replace_children(
- &mut self,
- mut range: Range<usize>,
- replacement: Vec<SyntaxNode>,
- ) -> NumberingResult {
- let superseded = &self.children[range.clone()];
-
- // Compute the new byte length.
- self.len = self.len + replacement.iter().map(SyntaxNode::len).sum::<usize>()
- - superseded.iter().map(SyntaxNode::len).sum::<usize>();
-
- // Compute the new number of descendants.
- self.descendants = self.descendants
- + replacement.iter().map(SyntaxNode::descendants).sum::<usize>()
- - superseded.iter().map(SyntaxNode::descendants).sum::<usize>();
-
- // Determine whether we're still erroneous after the replacement. That's
- // the case if
- // - any of the new nodes is erroneous,
- // - or if we were erroneous before due to a non-superseded node.
- self.erroneous = replacement.iter().any(SyntaxNode::erroneous)
- || (self.erroneous
- && (self.children[..range.start].iter().any(SyntaxNode::erroneous))
- || self.children[range.end..].iter().any(SyntaxNode::erroneous));
-
- // Perform the replacement.
- let replacement_count = replacement.len();
- self.children.splice(range.clone(), replacement);
- range.end = range.start + replacement_count;
-
- // Renumber the new children. Retries until it works, taking
- // exponentially more children into account.
- let mut left = 0;
- let mut right = 0;
- let max_left = range.start;
- let max_right = self.children.len() - range.end;
- loop {
- let renumber = range.start - left..range.end + right;
-
- // The minimum assignable number is either
- // - the upper bound of the node right before the to-be-renumbered
- // children,
- // - or this inner node's span number plus one if renumbering starts
- // at the first child.
- let start_number = renumber
- .start
- .checked_sub(1)
- .and_then(|i| self.children.get(i))
- .map_or(self.span.number() + 1, |child| child.upper());
-
- // The upper bound for renumbering is either
- // - the span number of the first child after the to-be-renumbered
- // children,
- // - or this node's upper bound if renumbering ends behind the last
- // child.
- let end_number = self
- .children
- .get(renumber.end)
- .map_or(self.upper, |next| next.span().number());
-
- // Try to renumber.
- let within = start_number..end_number;
- let id = self.span.id();
- if self.numberize(id, Some(renumber), within).is_ok() {
- return Ok(());
- }
-
- // If it didn't even work with all children, we give up.
- if left == max_left && right == max_right {
- return Err(Unnumberable);
- }
-
- // Exponential expansion to both sides.
- left = (left + 1).next_power_of_two().min(max_left);
- right = (right + 1).next_power_of_two().min(max_right);
- }
- }
-
- /// Update this node after changes were made to one of its children.
- fn update_parent(
- &mut self,
- prev_len: usize,
- new_len: usize,
- prev_descendants: usize,
- new_descendants: usize,
- ) {
- self.len = self.len + new_len - prev_len;
- self.descendants = self.descendants + new_descendants - prev_descendants;
- self.erroneous = self.children.iter().any(SyntaxNode::erroneous);
- }
-}
-
-impl Debug for InnerNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "{:?}: {}", self.kind, self.len)?;
- if !self.children.is_empty() {
- f.write_str(" ")?;
- f.debug_list().entries(&self.children).finish()?;
- }
- Ok(())
- }
-}
-
-/// An error node in the untyped syntax tree.
-#[derive(Clone, Eq, PartialEq, Hash)]
-struct ErrorNode {
- /// The error message.
- message: EcoString,
- /// The source text of the node.
- text: EcoString,
- /// The node's span.
- span: Span,
-}
-
-impl ErrorNode {
- /// Create new error node.
- fn new(message: impl Into<EcoString>, text: impl Into<EcoString>) -> Self {
- Self {
- message: message.into(),
- text: text.into(),
- span: Span::detached(),
- }
- }
-
- /// The byte length of the node in the source text.
- fn len(&self) -> usize {
- self.text.len()
- }
-}
-
-impl Debug for ErrorNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "Error: {:?} ({})", self.text, self.message)
- }
-}
-
-/// A syntax node in a context.
-///
-/// Knows its exact offset in the file and provides access to its
-/// children, parent and siblings.
-///
-/// **Note that all sibling and leaf accessors skip over trivia!**
-#[derive(Clone)]
-pub struct LinkedNode<'a> {
- node: &'a SyntaxNode,
- parent: Option<Rc<Self>>,
- index: usize,
- offset: usize,
-}
-
-impl<'a> LinkedNode<'a> {
- /// Start a new traversal at a root node.
- pub fn new(root: &'a SyntaxNode) -> Self {
- Self { node: root, parent: None, index: 0, offset: 0 }
- }
-
- /// Get the contained syntax node.
- pub fn get(&self) -> &'a SyntaxNode {
- self.node
- }
-
- /// The index of this node in its parent's children list.
- pub fn index(&self) -> usize {
- self.index
- }
-
- /// The absolute byte offset of this node in the source file.
- pub fn offset(&self) -> usize {
- self.offset
- }
-
- /// The byte range of this node in the source file.
- pub fn range(&self) -> Range<usize> {
- self.offset..self.offset + self.node.len()
- }
-
- /// An iterator over this node's children.
- pub fn children(&self) -> LinkedChildren<'a> {
- LinkedChildren {
- parent: Rc::new(self.clone()),
- iter: self.node.children().enumerate(),
- front: self.offset,
- back: self.offset + self.len(),
- }
- }
-
- /// Find a descendant with the given span.
- pub fn find(&self, span: Span) -> Option<LinkedNode<'a>> {
- if self.span() == span {
- return Some(self.clone());
- }
-
- if let Repr::Inner(inner) = &self.0 {
- // The parent of a subtree has a smaller span number than all of its
- // descendants. Therefore, we can bail out early if the target span's
- // number is smaller than our number.
- if span.number() < inner.span.number() {
- return None;
- }
-
- let mut children = self.children().peekable();
- while let Some(child) = children.next() {
- // Every node in this child's subtree has a smaller span number than
- // the next sibling. Therefore we only need to recurse if the next
- // sibling's span number is larger than the target span's number.
- if children
- .peek()
- .map_or(true, |next| next.span().number() > span.number())
- {
- if let Some(found) = child.find(span) {
- return Some(found);
- }
- }
- }
- }
-
- None
- }
-}
-
-/// Access to parents and siblings.
-impl<'a> LinkedNode<'a> {
- /// Get this node's parent.
- pub fn parent(&self) -> Option<&Self> {
- self.parent.as_deref()
- }
-
- /// Get the first previous non-trivia sibling node.
- pub fn prev_sibling(&self) -> Option<Self> {
- let parent = self.parent()?;
- let index = self.index.checked_sub(1)?;
- let node = parent.node.children().nth(index)?;
- let offset = self.offset - node.len();
- let prev = Self { node, parent: self.parent.clone(), index, offset };
- if prev.kind().is_trivia() {
- prev.prev_sibling()
- } else {
- Some(prev)
- }
- }
-
- /// Get the next non-trivia sibling node.
- pub fn next_sibling(&self) -> Option<Self> {
- let parent = self.parent()?;
- let index = self.index.checked_add(1)?;
- let node = parent.node.children().nth(index)?;
- let offset = self.offset + self.node.len();
- let next = Self { node, parent: self.parent.clone(), index, offset };
- if next.kind().is_trivia() {
- next.next_sibling()
- } else {
- Some(next)
- }
- }
-
- /// Get the kind of this node's parent.
- pub fn parent_kind(&self) -> Option<SyntaxKind> {
- Some(self.parent()?.node.kind())
- }
-
- /// Get the kind of this node's first previous non-trivia sibling.
- pub fn prev_sibling_kind(&self) -> Option<SyntaxKind> {
- Some(self.prev_sibling()?.node.kind())
- }
-
- /// Get the kind of this node's next non-trivia sibling.
- pub fn next_sibling_kind(&self) -> Option<SyntaxKind> {
- Some(self.next_sibling()?.node.kind())
- }
-}
-
-/// Access to leafs.
-impl<'a> LinkedNode<'a> {
- /// Get the rightmost non-trivia leaf before this node.
- pub fn prev_leaf(&self) -> Option<Self> {
- let mut node = self.clone();
- while let Some(prev) = node.prev_sibling() {
- if let Some(leaf) = prev.rightmost_leaf() {
- return Some(leaf);
- }
- node = prev;
- }
- self.parent()?.prev_leaf()
- }
-
- /// Find the leftmost contained non-trivia leaf.
- pub fn leftmost_leaf(&self) -> Option<Self> {
- if self.is_leaf() && !self.kind().is_trivia() && !self.kind().is_error() {
- return Some(self.clone());
- }
-
- for child in self.children() {
- if let Some(leaf) = child.leftmost_leaf() {
- return Some(leaf);
- }
- }
-
- None
- }
-
- /// Get the leaf at the specified byte offset.
- pub fn leaf_at(&self, cursor: usize) -> Option<Self> {
- if self.node.children().len() == 0 && cursor <= self.offset + self.len() {
- return Some(self.clone());
- }
-
- let mut offset = self.offset;
- let count = self.node.children().len();
- for (i, child) in self.children().enumerate() {
- let len = child.len();
- if (offset < cursor && cursor <= offset + len)
- || (offset == cursor && i + 1 == count)
- {
- return child.leaf_at(cursor);
- }
- offset += len;
- }
-
- None
- }
-
- /// Find the rightmost contained non-trivia leaf.
- pub fn rightmost_leaf(&self) -> Option<Self> {
- if self.is_leaf() && !self.kind().is_trivia() {
- return Some(self.clone());
- }
-
- for child in self.children().rev() {
- if let Some(leaf) = child.rightmost_leaf() {
- return Some(leaf);
- }
- }
-
- None
- }
-
- /// Get the leftmost non-trivia leaf after this node.
- pub fn next_leaf(&self) -> Option<Self> {
- let mut node = self.clone();
- while let Some(next) = node.next_sibling() {
- if let Some(leaf) = next.leftmost_leaf() {
- return Some(leaf);
- }
- node = next;
- }
- self.parent()?.next_leaf()
- }
-}
-
-impl Deref for LinkedNode<'_> {
- type Target = SyntaxNode;
-
- fn deref(&self) -> &Self::Target {
- self.get()
- }
-}
-
-impl Debug for LinkedNode<'_> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.node.fmt(f)
- }
-}
-
-/// An iterator over the children of a linked node.
-pub struct LinkedChildren<'a> {
- parent: Rc<LinkedNode<'a>>,
- iter: std::iter::Enumerate<std::slice::Iter<'a, SyntaxNode>>,
- front: usize,
- back: usize,
-}
-
-impl<'a> Iterator for LinkedChildren<'a> {
- type Item = LinkedNode<'a>;
-
- fn next(&mut self) -> Option<Self::Item> {
- self.iter.next().map(|(index, node)| {
- let offset = self.front;
- self.front += node.len();
- LinkedNode {
- node,
- parent: Some(self.parent.clone()),
- index,
- offset,
- }
- })
- }
-
- fn size_hint(&self) -> (usize, Option<usize>) {
- self.iter.size_hint()
- }
-}
-
-impl DoubleEndedIterator for LinkedChildren<'_> {
- fn next_back(&mut self) -> Option<Self::Item> {
- self.iter.next_back().map(|(index, node)| {
- self.back -= node.len();
- LinkedNode {
- node,
- parent: Some(self.parent.clone()),
- index,
- offset: self.back,
- }
- })
- }
-}
-
-impl ExactSizeIterator for LinkedChildren<'_> {}
-
-/// Result of numbering a node within an interval.
-pub(super) type NumberingResult = Result<(), Unnumberable>;
-
-/// Indicates that a node cannot be numbered within a given interval.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub(super) struct Unnumberable;
-
-impl Display for Unnumberable {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad("cannot number within this interval")
- }
-}
-
-impl std::error::Error for Unnumberable {}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::syntax::Source;
-
- #[test]
- fn test_linked_node() {
- let source = Source::detached("#set text(12pt, red)");
-
- // Find "text".
- let node = LinkedNode::new(source.root()).leaf_at(7).unwrap();
- assert_eq!(node.offset(), 5);
- assert_eq!(node.text(), "text");
-
- // Go back to "#set". Skips the space.
- let prev = node.prev_sibling().unwrap();
- assert_eq!(prev.offset(), 1);
- assert_eq!(prev.text(), "set");
- }
-
- #[test]
- fn test_linked_node_non_trivia_leaf() {
- let source = Source::detached("#set fun(12pt, red)");
- let leaf = LinkedNode::new(source.root()).leaf_at(6).unwrap();
- let prev = leaf.prev_leaf().unwrap();
- assert_eq!(leaf.text(), "fun");
- assert_eq!(prev.text(), "set");
-
- let source = Source::detached("#let x = 10");
- let leaf = LinkedNode::new(source.root()).leaf_at(9).unwrap();
- let prev = leaf.prev_leaf().unwrap();
- let next = leaf.next_leaf().unwrap();
- assert_eq!(prev.text(), "=");
- assert_eq!(leaf.text(), " ");
- assert_eq!(next.text(), "10");
- }
-}
diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs
deleted file mode 100644
index 54670df5..00000000
--- a/src/syntax/parser.rs
+++ /dev/null
@@ -1,1643 +0,0 @@
-use std::collections::HashSet;
-use std::ops::Range;
-
-use ecow::{eco_format, EcoString};
-use unicode_math_class::MathClass;
-
-use super::{ast, is_newline, LexMode, Lexer, SyntaxKind, SyntaxNode};
-
-/// Parse a source file.
-pub fn parse(text: &str) -> SyntaxNode {
- let mut p = Parser::new(text, 0, LexMode::Markup);
- markup(&mut p, true, 0, |_| false);
- p.finish().into_iter().next().unwrap()
-}
-
-/// Parse code directly.
-///
-/// This is only used for syntax highlighting.
-pub fn parse_code(text: &str) -> SyntaxNode {
- let mut p = Parser::new(text, 0, LexMode::Code);
- let m = p.marker();
- p.skip();
- code_exprs(&mut p, |_| false);
- p.wrap_skipless(m, SyntaxKind::Code);
- p.finish().into_iter().next().unwrap()
-}
-
-fn markup(
- p: &mut Parser,
- mut at_start: bool,
- min_indent: usize,
- mut stop: impl FnMut(&Parser) -> bool,
-) {
- let m = p.marker();
- let mut nesting: usize = 0;
- while !p.eof() {
- match p.current() {
- SyntaxKind::LeftBracket => nesting += 1,
- SyntaxKind::RightBracket if nesting > 0 => nesting -= 1,
- _ if stop(p) => break,
- _ => {}
- }
-
- if p.newline() {
- at_start = true;
- if min_indent > 0 && p.column(p.current_end()) < min_indent {
- break;
- }
- p.eat();
- continue;
- }
-
- let prev = p.prev_end();
- markup_expr(p, &mut at_start);
- if !p.progress(prev) {
- p.unexpected();
- }
- }
- p.wrap(m, SyntaxKind::Markup);
-}
-
-pub(super) fn reparse_markup(
- text: &str,
- range: Range<usize>,
- at_start: &mut bool,
- nesting: &mut usize,
- mut stop: impl FnMut(SyntaxKind) -> bool,
-) -> Option<Vec<SyntaxNode>> {
- let mut p = Parser::new(text, range.start, LexMode::Markup);
- while !p.eof() && p.current_start() < range.end {
- match p.current() {
- SyntaxKind::LeftBracket => *nesting += 1,
- SyntaxKind::RightBracket if *nesting > 0 => *nesting -= 1,
- _ if stop(p.current()) => break,
- _ => {}
- }
-
- if p.newline() {
- *at_start = true;
- p.eat();
- continue;
- }
-
- let prev = p.prev_end();
- markup_expr(&mut p, at_start);
- if !p.progress(prev) {
- p.unexpected();
- }
- }
- (p.balanced && p.current_start() == range.end).then(|| p.finish())
-}
-
-fn markup_expr(p: &mut Parser, at_start: &mut bool) {
- match p.current() {
- SyntaxKind::Space
- | SyntaxKind::Parbreak
- | SyntaxKind::LineComment
- | SyntaxKind::BlockComment => {
- p.eat();
- return;
- }
-
- SyntaxKind::Text
- | SyntaxKind::Linebreak
- | SyntaxKind::Escape
- | SyntaxKind::Shorthand
- | SyntaxKind::SmartQuote
- | SyntaxKind::Raw
- | SyntaxKind::Link
- | SyntaxKind::Label => p.eat(),
-
- SyntaxKind::Hashtag => embedded_code_expr(p),
- SyntaxKind::Star => strong(p),
- SyntaxKind::Underscore => emph(p),
- SyntaxKind::HeadingMarker if *at_start => heading(p),
- SyntaxKind::ListMarker if *at_start => list_item(p),
- SyntaxKind::EnumMarker if *at_start => enum_item(p),
- SyntaxKind::TermMarker if *at_start => term_item(p),
- SyntaxKind::RefMarker => reference(p),
- SyntaxKind::Dollar => equation(p),
-
- SyntaxKind::LeftBracket
- | SyntaxKind::RightBracket
- | SyntaxKind::HeadingMarker
- | SyntaxKind::ListMarker
- | SyntaxKind::EnumMarker
- | SyntaxKind::TermMarker
- | SyntaxKind::Colon => p.convert(SyntaxKind::Text),
-
- _ => {}
- }
-
- *at_start = false;
-}
-
-fn strong(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Star);
- markup(p, false, 0, |p| {
- p.at(SyntaxKind::Star)
- || p.at(SyntaxKind::Parbreak)
- || p.at(SyntaxKind::RightBracket)
- });
- p.expect_closing_delimiter(m, SyntaxKind::Star);
- p.wrap(m, SyntaxKind::Strong);
-}
-
-fn emph(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Underscore);
- markup(p, false, 0, |p| {
- p.at(SyntaxKind::Underscore)
- || p.at(SyntaxKind::Parbreak)
- || p.at(SyntaxKind::RightBracket)
- });
- p.expect_closing_delimiter(m, SyntaxKind::Underscore);
- p.wrap(m, SyntaxKind::Emph);
-}
-
-fn heading(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::HeadingMarker);
- whitespace_line(p);
- markup(p, false, usize::MAX, |p| {
- p.at(SyntaxKind::Label)
- || p.at(SyntaxKind::RightBracket)
- || (p.at(SyntaxKind::Space) && p.lexer.clone().next() == SyntaxKind::Label)
- });
- p.wrap(m, SyntaxKind::Heading);
-}
-
-fn list_item(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::ListMarker);
- let min_indent = p.column(p.prev_end());
- whitespace_line(p);
- markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket));
- p.wrap(m, SyntaxKind::ListItem);
-}
-
-fn enum_item(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::EnumMarker);
- let min_indent = p.column(p.prev_end());
- whitespace_line(p);
- markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket));
- p.wrap(m, SyntaxKind::EnumItem);
-}
-
-fn term_item(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::TermMarker);
- let min_indent = p.column(p.prev_end());
- whitespace_line(p);
- markup(p, false, usize::MAX, |p| {
- p.at(SyntaxKind::Colon) || p.at(SyntaxKind::RightBracket)
- });
- p.expect(SyntaxKind::Colon);
- whitespace_line(p);
- markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket));
- p.wrap(m, SyntaxKind::TermItem);
-}
-
-fn reference(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::RefMarker);
- if p.directly_at(SyntaxKind::LeftBracket) {
- content_block(p);
- }
- p.wrap(m, SyntaxKind::Ref);
-}
-
-fn whitespace_line(p: &mut Parser) {
- while !p.newline() && p.current().is_trivia() {
- p.eat();
- }
-}
-
-fn equation(p: &mut Parser) {
- let m = p.marker();
- p.enter(LexMode::Math);
- p.assert(SyntaxKind::Dollar);
- math(p, |p| p.at(SyntaxKind::Dollar));
- p.expect_closing_delimiter(m, SyntaxKind::Dollar);
- p.exit();
- p.wrap(m, SyntaxKind::Equation);
-}
-
-fn math(p: &mut Parser, mut stop: impl FnMut(&Parser) -> bool) {
- let m = p.marker();
- while !p.eof() && !stop(p) {
- let prev = p.prev_end();
- math_expr(p);
- if !p.progress(prev) {
- p.unexpected();
- }
- }
- p.wrap(m, SyntaxKind::Math);
-}
-
-fn math_expr(p: &mut Parser) {
- math_expr_prec(p, 0, SyntaxKind::Eof)
-}
-
-fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
- let m = p.marker();
- let mut continuable = false;
- match p.current() {
- SyntaxKind::Hashtag => embedded_code_expr(p),
- SyntaxKind::MathIdent => {
- continuable = true;
- p.eat();
- while p.directly_at(SyntaxKind::Text)
- && p.current_text() == "."
- && matches!(
- p.lexer.clone().next(),
- SyntaxKind::MathIdent | SyntaxKind::Text
- )
- {
- p.convert(SyntaxKind::Dot);
- p.convert(SyntaxKind::Ident);
- p.wrap(m, SyntaxKind::FieldAccess);
- }
- if min_prec < 3 && p.directly_at(SyntaxKind::Text) && p.current_text() == "("
- {
- math_args(p);
- p.wrap(m, SyntaxKind::FuncCall);
- continuable = false;
- }
- }
-
- SyntaxKind::Text | SyntaxKind::Shorthand => {
- continuable = matches!(
- math_class(p.current_text()),
- None | Some(MathClass::Alphabetic)
- );
- if !maybe_delimited(p, true) {
- p.eat();
- }
- }
-
- SyntaxKind::Linebreak | SyntaxKind::MathAlignPoint => p.eat(),
- SyntaxKind::Escape | SyntaxKind::Str => {
- continuable = true;
- p.eat();
- }
-
- SyntaxKind::Root => {
- if min_prec < 3 {
- p.eat();
- let m2 = p.marker();
- math_expr_prec(p, 2, stop);
- math_unparen(p, m2);
- p.wrap(m, SyntaxKind::MathRoot);
- }
- }
-
- _ => p.expected("expression"),
- }
-
- if continuable
- && min_prec < 3
- && p.prev_end() == p.current_start()
- && maybe_delimited(p, false)
- {
- p.wrap(m, SyntaxKind::Math);
- }
-
- while !p.eof() && !p.at(stop) {
- if p.directly_at(SyntaxKind::Text) && p.current_text() == "!" {
- p.eat();
- p.wrap(m, SyntaxKind::Math);
- continue;
- }
-
- let Some((kind, stop, assoc, mut prec)) = math_op(p.current()) else {
- break;
- };
-
- if prec < min_prec {
- break;
- }
-
- match assoc {
- ast::Assoc::Left => prec += 1,
- ast::Assoc::Right => {}
- }
-
- if kind == SyntaxKind::MathFrac {
- math_unparen(p, m);
- }
-
- p.eat();
- let m2 = p.marker();
- math_expr_prec(p, prec, stop);
- math_unparen(p, m2);
-
- if p.eat_if(SyntaxKind::Underscore) || p.eat_if(SyntaxKind::Hat) {
- let m3 = p.marker();
- math_expr_prec(p, prec, SyntaxKind::Eof);
- math_unparen(p, m3);
- }
-
- p.wrap(m, kind);
- }
-}
-
-fn maybe_delimited(p: &mut Parser, allow_fence: bool) -> bool {
- if allow_fence && math_class(p.current_text()) == Some(MathClass::Fence) {
- math_delimited(p, MathClass::Fence);
- true
- } else if math_class(p.current_text()) == Some(MathClass::Opening) {
- math_delimited(p, MathClass::Closing);
- true
- } else {
- false
- }
-}
-
-fn math_delimited(p: &mut Parser, stop: MathClass) {
- let m = p.marker();
- p.eat();
- let m2 = p.marker();
- while !p.eof() && !p.at(SyntaxKind::Dollar) {
- let class = math_class(p.current_text());
- if stop == MathClass::Fence && class == Some(MathClass::Closing) {
- break;
- }
-
- if class == Some(stop) {
- p.wrap(m2, SyntaxKind::Math);
- p.eat();
- p.wrap(m, SyntaxKind::MathDelimited);
- return;
- }
-
- let prev = p.prev_end();
- math_expr(p);
- if !p.progress(prev) {
- p.unexpected();
- }
- }
-
- p.wrap(m, SyntaxKind::Math);
-}
-
-fn math_unparen(p: &mut Parser, m: Marker) {
- let Some(node) = p.nodes.get_mut(m.0) else { return };
- if node.kind() != SyntaxKind::MathDelimited {
- return;
- }
-
- if let [first, .., last] = node.children_mut() {
- if first.text() == "(" && last.text() == ")" {
- first.convert_to_kind(SyntaxKind::LeftParen);
- last.convert_to_kind(SyntaxKind::RightParen);
- }
- }
-
- node.convert_to_kind(SyntaxKind::Math);
-}
-
-fn math_class(text: &str) -> Option<MathClass> {
- match text {
- "[|" => return Some(MathClass::Opening),
- "|]" => return Some(MathClass::Closing),
- "||" => return Some(MathClass::Fence),
- _ => {}
- }
-
- let mut chars = text.chars();
- chars
- .next()
- .filter(|_| chars.next().is_none())
- .and_then(unicode_math_class::class)
-}
-
-fn math_op(kind: SyntaxKind) -> Option<(SyntaxKind, SyntaxKind, ast::Assoc, usize)> {
- match kind {
- SyntaxKind::Underscore => {
- Some((SyntaxKind::MathAttach, SyntaxKind::Hat, ast::Assoc::Right, 2))
- }
- SyntaxKind::Hat => {
- Some((SyntaxKind::MathAttach, SyntaxKind::Underscore, ast::Assoc::Right, 2))
- }
- SyntaxKind::Slash => {
- Some((SyntaxKind::MathFrac, SyntaxKind::Eof, ast::Assoc::Left, 1))
- }
- _ => None,
- }
-}
-
-fn math_args(p: &mut Parser) {
- let m = p.marker();
- p.convert(SyntaxKind::LeftParen);
-
- let mut namable = true;
- let mut named = None;
- let mut has_arrays = false;
- let mut array = p.marker();
- let mut arg = p.marker();
-
- while !p.eof() && !p.at(SyntaxKind::Dollar) {
- if namable
- && (p.at(SyntaxKind::MathIdent) || p.at(SyntaxKind::Text))
- && p.text[p.current_end()..].starts_with(':')
- {
- p.convert(SyntaxKind::Ident);
- p.convert(SyntaxKind::Colon);
- named = Some(arg);
- arg = p.marker();
- array = p.marker();
- }
-
- match p.current_text() {
- ")" => break,
- ";" => {
- maybe_wrap_in_math(p, arg, named);
- p.wrap(array, SyntaxKind::Array);
- p.convert(SyntaxKind::Semicolon);
- array = p.marker();
- arg = p.marker();
- namable = true;
- named = None;
- has_arrays = true;
- continue;
- }
- "," => {
- maybe_wrap_in_math(p, arg, named);
- p.convert(SyntaxKind::Comma);
- arg = p.marker();
- namable = true;
- if named.is_some() {
- array = p.marker();
- named = None;
- }
- continue;
- }
- _ => {}
- }
-
- let prev = p.prev_end();
- math_expr(p);
- if !p.progress(prev) {
- p.unexpected();
- }
-
- namable = false;
- }
-
- if arg != p.marker() {
- maybe_wrap_in_math(p, arg, named);
- if named.is_some() {
- array = p.marker();
- }
- }
-
- if has_arrays && array != p.marker() {
- p.wrap(array, SyntaxKind::Array);
- }
-
- if p.at(SyntaxKind::Text) && p.current_text() == ")" {
- p.convert(SyntaxKind::RightParen);
- } else {
- p.expected("closing paren");
- p.balanced = false;
- }
-
- p.wrap(m, SyntaxKind::Args);
-}
-
-fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, named: Option<Marker>) {
- let exprs = p.post_process(arg).filter(|node| node.is::<ast::Expr>()).count();
- if exprs != 1 {
- p.wrap(arg, SyntaxKind::Math);
- }
-
- if let Some(m) = named {
- p.wrap(m, SyntaxKind::Named);
- }
-}
-
-fn code(p: &mut Parser, stop: impl FnMut(&Parser) -> bool) {
- let m = p.marker();
- code_exprs(p, stop);
- p.wrap(m, SyntaxKind::Code);
-}
-
-fn code_exprs(p: &mut Parser, mut stop: impl FnMut(&Parser) -> bool) {
- while !p.eof() && !stop(p) {
- p.stop_at_newline(true);
- let prev = p.prev_end();
- code_expr(p);
- if p.progress(prev) && !p.eof() && !stop(p) && !p.eat_if(SyntaxKind::Semicolon) {
- p.expected("semicolon or line break");
- }
- p.unstop();
- if !p.progress(prev) && !p.eof() {
- p.unexpected();
- }
- }
-}
-
-fn code_expr(p: &mut Parser) {
- code_expr_prec(p, false, 0, false)
-}
-
-fn code_expr_or_pattern(p: &mut Parser) {
- code_expr_prec(p, false, 0, true)
-}
-
-fn embedded_code_expr(p: &mut Parser) {
- p.stop_at_newline(true);
- p.enter(LexMode::Code);
- p.assert(SyntaxKind::Hashtag);
- p.unskip();
-
- let stmt = matches!(
- p.current(),
- SyntaxKind::Let
- | SyntaxKind::Set
- | SyntaxKind::Show
- | SyntaxKind::Import
- | SyntaxKind::Include
- );
-
- let prev = p.prev_end();
- code_expr_prec(p, true, 0, false);
-
- // Consume error for things like `#12p` or `#"abc\"`.
- if !p.progress(prev) {
- p.unexpected();
- }
-
- let semi =
- (stmt || p.directly_at(SyntaxKind::Semicolon)) && p.eat_if(SyntaxKind::Semicolon);
-
- if stmt && !semi && !p.eof() && !p.at(SyntaxKind::RightBracket) {
- p.expected("semicolon or line break");
- }
-
- p.exit();
- p.unstop();
-}
-
-fn code_expr_prec(
- p: &mut Parser,
- atomic: bool,
- min_prec: usize,
- allow_destructuring: bool,
-) {
- let m = p.marker();
- if let (false, Some(op)) = (atomic, ast::UnOp::from_kind(p.current())) {
- p.eat();
- code_expr_prec(p, atomic, op.precedence(), false);
- p.wrap(m, SyntaxKind::Unary);
- } else {
- code_primary(p, atomic, allow_destructuring);
- }
-
- loop {
- if p.directly_at(SyntaxKind::LeftParen) || p.directly_at(SyntaxKind::LeftBracket)
- {
- args(p);
- p.wrap(m, SyntaxKind::FuncCall);
- continue;
- }
-
- let at_field_or_method =
- p.directly_at(SyntaxKind::Dot) && p.lexer.clone().next() == SyntaxKind::Ident;
-
- if atomic && !at_field_or_method {
- break;
- }
-
- if p.eat_if(SyntaxKind::Dot) {
- p.expect(SyntaxKind::Ident);
- p.wrap(m, SyntaxKind::FieldAccess);
- continue;
- }
-
- let binop =
- if ast::BinOp::NotIn.precedence() >= min_prec && p.eat_if(SyntaxKind::Not) {
- if p.at(SyntaxKind::In) {
- Some(ast::BinOp::NotIn)
- } else {
- p.expected("keyword `in`");
- break;
- }
- } else {
- ast::BinOp::from_kind(p.current())
- };
-
- if let Some(op) = binop {
- let mut prec = op.precedence();
- if prec < min_prec {
- break;
- }
-
- match op.assoc() {
- ast::Assoc::Left => prec += 1,
- ast::Assoc::Right => {}
- }
-
- p.eat();
- code_expr_prec(p, false, prec, false);
- p.wrap(m, SyntaxKind::Binary);
- continue;
- }
-
- break;
- }
-}
-
-fn code_primary(p: &mut Parser, atomic: bool, allow_destructuring: bool) {
- let m = p.marker();
- match p.current() {
- SyntaxKind::Ident => {
- p.eat();
- if !atomic && p.at(SyntaxKind::Arrow) {
- p.wrap(m, SyntaxKind::Params);
- p.assert(SyntaxKind::Arrow);
- code_expr(p);
- p.wrap(m, SyntaxKind::Closure);
- }
- }
- SyntaxKind::Underscore if !atomic => {
- p.eat();
- if p.at(SyntaxKind::Arrow) {
- p.wrap(m, SyntaxKind::Params);
- p.eat();
- code_expr(p);
- p.wrap(m, SyntaxKind::Closure);
- } else if let Some(underscore) = p.node_mut(m) {
- underscore.convert_to_error("expected expression, found underscore");
- }
- }
-
- SyntaxKind::LeftBrace => code_block(p),
- SyntaxKind::LeftBracket => content_block(p),
- SyntaxKind::LeftParen => with_paren(p, allow_destructuring),
- SyntaxKind::Dollar => equation(p),
- SyntaxKind::Let => let_binding(p),
- SyntaxKind::Set => set_rule(p),
- SyntaxKind::Show => show_rule(p),
- SyntaxKind::If => conditional(p),
- SyntaxKind::While => while_loop(p),
- SyntaxKind::For => for_loop(p),
- SyntaxKind::Import => module_import(p),
- SyntaxKind::Include => module_include(p),
- SyntaxKind::Break => break_stmt(p),
- SyntaxKind::Continue => continue_stmt(p),
- SyntaxKind::Return => return_stmt(p),
-
- SyntaxKind::None
- | SyntaxKind::Auto
- | SyntaxKind::Int
- | SyntaxKind::Float
- | SyntaxKind::Bool
- | SyntaxKind::Numeric
- | SyntaxKind::Str
- | SyntaxKind::Label
- | SyntaxKind::Raw => p.eat(),
-
- _ => p.expected("expression"),
- }
-}
-
-fn block(p: &mut Parser) {
- match p.current() {
- SyntaxKind::LeftBracket => content_block(p),
- SyntaxKind::LeftBrace => code_block(p),
- _ => p.expected("block"),
- }
-}
-
-pub(super) fn reparse_block(text: &str, range: Range<usize>) -> Option<SyntaxNode> {
- let mut p = Parser::new(text, range.start, LexMode::Code);
- assert!(p.at(SyntaxKind::LeftBracket) || p.at(SyntaxKind::LeftBrace));
- block(&mut p);
- (p.balanced && p.prev_end() == range.end)
- .then(|| p.finish().into_iter().next().unwrap())
-}
-
-fn code_block(p: &mut Parser) {
- let m = p.marker();
- p.enter(LexMode::Code);
- p.stop_at_newline(false);
- p.assert(SyntaxKind::LeftBrace);
- code(p, |p| {
- p.at(SyntaxKind::RightBrace)
- || p.at(SyntaxKind::RightBracket)
- || p.at(SyntaxKind::RightParen)
- });
- p.expect_closing_delimiter(m, SyntaxKind::RightBrace);
- p.exit();
- p.unstop();
- p.wrap(m, SyntaxKind::CodeBlock);
-}
-
-fn content_block(p: &mut Parser) {
- let m = p.marker();
- p.enter(LexMode::Markup);
- p.assert(SyntaxKind::LeftBracket);
- markup(p, true, 0, |p| p.at(SyntaxKind::RightBracket));
- p.expect_closing_delimiter(m, SyntaxKind::RightBracket);
- p.exit();
- p.wrap(m, SyntaxKind::ContentBlock);
-}
-
-fn with_paren(p: &mut Parser, allow_destructuring: bool) {
- let m = p.marker();
- let mut kind = collection(p, true);
- if p.at(SyntaxKind::Arrow) {
- validate_params_at(p, m);
- p.wrap(m, SyntaxKind::Params);
- p.assert(SyntaxKind::Arrow);
- code_expr(p);
- kind = SyntaxKind::Closure;
- } else if p.at(SyntaxKind::Eq) && kind != SyntaxKind::Parenthesized {
- // TODO: add warning if p.at(SyntaxKind::Eq) && kind == SyntaxKind::Parenthesized
-
- validate_pattern_at(p, m, false);
- p.wrap(m, SyntaxKind::Destructuring);
- p.assert(SyntaxKind::Eq);
- code_expr(p);
- kind = SyntaxKind::DestructAssignment;
- }
-
- match kind {
- SyntaxKind::Array if !allow_destructuring => validate_array_at(p, m),
- SyntaxKind::Dict if !allow_destructuring => validate_dict_at(p, m),
- SyntaxKind::Parenthesized if !allow_destructuring => {
- validate_parenthesized_at(p, m)
- }
- SyntaxKind::Destructuring if !allow_destructuring => {
- invalidate_destructuring(p, m)
- }
- _ => {}
- }
- p.wrap(m, kind);
-}
-
-fn invalidate_destructuring(p: &mut Parser, m: Marker) {
- let mut collection_kind = Option::None;
- for child in p.post_process(m) {
- match child.kind() {
- SyntaxKind::Named | SyntaxKind::Keyed => match collection_kind {
- Some(SyntaxKind::Array) => child.convert_to_error(eco_format!(
- "expected expression, found {}",
- child.kind().name()
- )),
- _ => collection_kind = Some(SyntaxKind::Dict),
- },
- SyntaxKind::LeftParen | SyntaxKind::RightParen | SyntaxKind::Comma => {}
- kind => match collection_kind {
- Some(SyntaxKind::Dict) => child.convert_to_error(eco_format!(
- "expected named or keyed pair, found {}",
- kind.name()
- )),
- _ => collection_kind = Some(SyntaxKind::Array),
- },
- }
- }
-}
-
-fn collection(p: &mut Parser, keyed: bool) -> SyntaxKind {
- p.stop_at_newline(false);
-
- let m = p.marker();
- p.assert(SyntaxKind::LeftParen);
-
- let mut count = 0;
- let mut parenthesized = true;
- let mut kind = None;
- if keyed && p.eat_if(SyntaxKind::Colon) {
- kind = Some(SyntaxKind::Dict);
- parenthesized = false;
- }
-
- while !p.current().is_terminator() {
- let prev = p.prev_end();
- match item(p, keyed) {
- SyntaxKind::Spread => parenthesized = false,
- SyntaxKind::Named | SyntaxKind::Keyed => {
- match kind {
- Some(SyntaxKind::Array) => kind = Some(SyntaxKind::Destructuring),
- _ => kind = Some(SyntaxKind::Dict),
- }
- parenthesized = false;
- }
- SyntaxKind::Int => match kind {
- Some(SyntaxKind::Array) | None => kind = Some(SyntaxKind::Array),
- Some(_) => kind = Some(SyntaxKind::Destructuring),
- },
- _ if kind.is_none() => kind = Some(SyntaxKind::Array),
- _ => {}
- }
-
- if !p.progress(prev) {
- p.unexpected();
- continue;
- }
-
- count += 1;
-
- if p.current().is_terminator() {
- break;
- }
-
- if p.expect(SyntaxKind::Comma) {
- parenthesized = false;
- }
- }
-
- p.expect_closing_delimiter(m, SyntaxKind::RightParen);
- p.unstop();
-
- if parenthesized && count == 1 {
- SyntaxKind::Parenthesized
- } else {
- kind.unwrap_or(SyntaxKind::Array)
- }
-}
-
-fn item(p: &mut Parser, keyed: bool) -> SyntaxKind {
- let m = p.marker();
-
- if p.eat_if(SyntaxKind::Dots) {
- if p.at(SyntaxKind::Comma) || p.at(SyntaxKind::RightParen) {
- p.wrap(m, SyntaxKind::Spread);
- return SyntaxKind::Spread;
- }
-
- code_expr(p);
- p.wrap(m, SyntaxKind::Spread);
- return SyntaxKind::Spread;
- }
-
- if p.at(SyntaxKind::Underscore) {
- // This is a temporary workaround to fix `v.map(_ => {})`.
- let mut lexer = p.lexer.clone();
- let next =
- std::iter::from_fn(|| Some(lexer.next())).find(|kind| !kind.is_trivia());
- if next != Some(SyntaxKind::Arrow) {
- p.eat();
- return SyntaxKind::Underscore;
- }
- }
-
- code_expr_or_pattern(p);
-
- if !p.eat_if(SyntaxKind::Colon) {
- return SyntaxKind::Int;
- }
-
- if !p.eat_if(SyntaxKind::Underscore) {
- code_expr(p);
- }
-
- let kind = match p.node(m).map(SyntaxNode::kind) {
- Some(SyntaxKind::Ident) => SyntaxKind::Named,
- Some(SyntaxKind::Str) if keyed => SyntaxKind::Keyed,
- _ => {
- for child in p.post_process(m) {
- if child.kind() == SyntaxKind::Colon {
- break;
- }
-
- let mut message = EcoString::from("expected identifier");
- if keyed {
- message.push_str(" or string");
- }
- message.push_str(", found ");
- message.push_str(child.kind().name());
- child.convert_to_error(message);
- }
- SyntaxKind::Named
- }
- };
-
- p.wrap(m, kind);
- kind
-}
-
-fn args(p: &mut Parser) {
- if !p.at(SyntaxKind::LeftParen) && !p.at(SyntaxKind::LeftBracket) {
- p.expected("argument list");
- }
-
- let m = p.marker();
- if p.at(SyntaxKind::LeftParen) {
- collection(p, false);
- validate_args_at(p, m);
- }
-
- while p.directly_at(SyntaxKind::LeftBracket) {
- content_block(p);
- }
-
- p.wrap(m, SyntaxKind::Args);
-}
-
-enum PatternKind {
- Ident,
- Placeholder,
- Destructuring,
-}
-
-fn pattern(p: &mut Parser) -> PatternKind {
- let m = p.marker();
- if p.at(SyntaxKind::LeftParen) {
- let kind = collection(p, false);
- validate_pattern_at(p, m, true);
-
- if kind == SyntaxKind::Parenthesized {
- PatternKind::Ident
- } else {
- p.wrap(m, SyntaxKind::Destructuring);
- PatternKind::Destructuring
- }
- } else if p.eat_if(SyntaxKind::Underscore) {
- PatternKind::Placeholder
- } else {
- p.expect(SyntaxKind::Ident);
- PatternKind::Ident
- }
-}
-
-fn let_binding(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Let);
-
- let m2 = p.marker();
- let mut closure = false;
- let mut destructuring = false;
- match pattern(p) {
- PatternKind::Ident => {
- closure = p.directly_at(SyntaxKind::LeftParen);
- if closure {
- let m3 = p.marker();
- collection(p, false);
- validate_params_at(p, m3);
- p.wrap(m3, SyntaxKind::Params);
- }
- }
- PatternKind::Placeholder => {}
- PatternKind::Destructuring => destructuring = true,
- }
-
- let f = if closure || destructuring { Parser::expect } else { Parser::eat_if };
- if f(p, SyntaxKind::Eq) {
- code_expr(p);
- }
-
- if closure {
- p.wrap(m2, SyntaxKind::Closure);
- }
-
- p.wrap(m, SyntaxKind::LetBinding);
-}
-
-fn set_rule(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Set);
-
- let m2 = p.marker();
- p.expect(SyntaxKind::Ident);
- while p.eat_if(SyntaxKind::Dot) {
- p.expect(SyntaxKind::Ident);
- p.wrap(m2, SyntaxKind::FieldAccess);
- }
-
- args(p);
- if p.eat_if(SyntaxKind::If) {
- code_expr(p);
- }
- p.wrap(m, SyntaxKind::SetRule);
-}
-
-fn show_rule(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Show);
- p.unskip();
- let m2 = p.marker();
- p.skip();
-
- if !p.at(SyntaxKind::Colon) {
- code_expr(p);
- }
-
- if p.eat_if(SyntaxKind::Colon) {
- code_expr(p);
- } else {
- p.expected_at(m2, "colon");
- }
-
- p.wrap(m, SyntaxKind::ShowRule);
-}
-
-fn conditional(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::If);
- code_expr(p);
- block(p);
- if p.eat_if(SyntaxKind::Else) {
- if p.at(SyntaxKind::If) {
- conditional(p);
- } else {
- block(p);
- }
- }
- p.wrap(m, SyntaxKind::Conditional);
-}
-
-fn while_loop(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::While);
- code_expr(p);
- block(p);
- p.wrap(m, SyntaxKind::WhileLoop);
-}
-
-fn for_loop(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::For);
- pattern(p);
- if p.at(SyntaxKind::Comma) {
- p.expected("keyword `in` - did you mean to use a destructuring pattern?");
- if !p.eat_if(SyntaxKind::Ident) {
- p.eat_if(SyntaxKind::Underscore);
- }
- p.eat_if(SyntaxKind::In);
- } else {
- p.expect(SyntaxKind::In);
- }
- code_expr(p);
- block(p);
- p.wrap(m, SyntaxKind::ForLoop);
-}
-
-fn module_import(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Import);
- code_expr(p);
- if p.eat_if(SyntaxKind::Colon) && !p.eat_if(SyntaxKind::Star) {
- import_items(p);
- }
- p.wrap(m, SyntaxKind::ModuleImport);
-}
-
-fn import_items(p: &mut Parser) {
- let m = p.marker();
- while !p.eof() && !p.at(SyntaxKind::Semicolon) {
- if !p.eat_if(SyntaxKind::Ident) {
- p.unexpected();
- }
- if p.current().is_terminator() {
- break;
- }
- p.expect(SyntaxKind::Comma);
- }
- p.wrap(m, SyntaxKind::ImportItems);
-}
-
-fn module_include(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Include);
- code_expr(p);
- p.wrap(m, SyntaxKind::ModuleInclude);
-}
-
-fn break_stmt(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Break);
- p.wrap(m, SyntaxKind::LoopBreak);
-}
-
-fn continue_stmt(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Continue);
- p.wrap(m, SyntaxKind::LoopContinue);
-}
-
-fn return_stmt(p: &mut Parser) {
- let m = p.marker();
- p.assert(SyntaxKind::Return);
- if !p.current().is_terminator() && !p.at(SyntaxKind::Comma) {
- code_expr(p);
- }
- p.wrap(m, SyntaxKind::FuncReturn);
-}
-
-fn validate_parenthesized_at(p: &mut Parser, m: Marker) {
- for child in p.post_process(m) {
- let kind = child.kind();
- match kind {
- SyntaxKind::Array => validate_array(child.children_mut().iter_mut()),
- SyntaxKind::Dict => validate_dict(child.children_mut().iter_mut()),
- SyntaxKind::Underscore => {
- child.convert_to_error(eco_format!(
- "expected expression, found {}",
- kind.name()
- ));
- }
- _ => {}
- }
- }
-}
-
-fn validate_array_at(p: &mut Parser, m: Marker) {
- validate_array(p.post_process(m))
-}
-
-fn validate_array<'a>(children: impl Iterator<Item = &'a mut SyntaxNode>) {
- for child in children {
- let kind = child.kind();
- match kind {
- SyntaxKind::Array => validate_array(child.children_mut().iter_mut()),
- SyntaxKind::Dict => validate_dict(child.children_mut().iter_mut()),
- SyntaxKind::Named | SyntaxKind::Keyed | SyntaxKind::Underscore => {
- child.convert_to_error(eco_format!(
- "expected expression, found {}",
- kind.name()
- ));
- }
- _ => {}
- }
- }
-}
-
-fn validate_dict_at(p: &mut Parser, m: Marker) {
- validate_dict(p.post_process(m))
-}
-
-fn validate_dict<'a>(children: impl Iterator<Item = &'a mut SyntaxNode>) {
- let mut used = HashSet::new();
- for child in children {
- match child.kind() {
- SyntaxKind::Named | SyntaxKind::Keyed => {
- let Some(first) = child.children_mut().first_mut() else { continue };
- let key = match first.cast::<ast::Str>() {
- Some(str) => str.get(),
- None => first.text().clone(),
- };
-
- if !used.insert(key.clone()) {
- first.convert_to_error(eco_format!("duplicate key: {key}"));
- child.make_erroneous();
- }
- }
- SyntaxKind::Spread => {}
- SyntaxKind::LeftParen
- | SyntaxKind::RightParen
- | SyntaxKind::Comma
- | SyntaxKind::Colon => {}
- kind => {
- child.convert_to_error(eco_format!(
- "expected named or keyed pair, found {}",
- kind.name()
- ));
- }
- }
- }
-}
-
-fn validate_params_at(p: &mut Parser, m: Marker) {
- let mut used_spread = false;
- let mut used = HashSet::new();
- for child in p.post_process(m) {
- match child.kind() {
- SyntaxKind::Ident => {
- if !used.insert(child.text().clone()) {
- child.convert_to_error(eco_format!(
- "duplicate parameter: {}",
- child.text()
- ));
- }
- }
- SyntaxKind::Named => {
- let Some(within) = child.children_mut().first_mut() else { return };
- if !used.insert(within.text().clone()) {
- within.convert_to_error(eco_format!(
- "duplicate parameter: {}",
- within.text()
- ));
- child.make_erroneous();
- }
- }
- SyntaxKind::Spread => {
- let Some(within) = child.children_mut().last_mut() else { continue };
- if used_spread {
- child.convert_to_error("only one argument sink is allowed");
- continue;
- }
- used_spread = true;
- if within.kind() == SyntaxKind::Dots {
- continue;
- } else if within.kind() != SyntaxKind::Ident {
- within.convert_to_error(eco_format!(
- "expected identifier, found {}",
- within.kind().name(),
- ));
- child.make_erroneous();
- continue;
- }
- if !used.insert(within.text().clone()) {
- within.convert_to_error(eco_format!(
- "duplicate parameter: {}",
- within.text()
- ));
- child.make_erroneous();
- }
- }
- SyntaxKind::Array | SyntaxKind::Dict | SyntaxKind::Destructuring => {
- validate_pattern(child.children_mut().iter_mut(), &mut used, false);
- child.convert_to_kind(SyntaxKind::Destructuring);
- }
- SyntaxKind::LeftParen
- | SyntaxKind::RightParen
- | SyntaxKind::Comma
- | SyntaxKind::Underscore => {}
- kind => {
- child.convert_to_error(eco_format!(
- "expected identifier, named pair or argument sink, found {}",
- kind.name()
- ));
- }
- }
- }
-}
-
-fn validate_args_at(p: &mut Parser, m: Marker) {
- let mut used = HashSet::new();
- for child in p.post_process(m) {
- if child.kind() == SyntaxKind::Named {
- let Some(within) = child.children_mut().first_mut() else { return };
- if !used.insert(within.text().clone()) {
- within.convert_to_error(eco_format!(
- "duplicate argument: {}",
- within.text()
- ));
- child.make_erroneous();
- }
- } else if child.kind() == SyntaxKind::Underscore {
- child.convert_to_error("unexpected underscore");
- }
- }
-}
-
-fn validate_pattern_at(p: &mut Parser, m: Marker, forbid_expressions: bool) {
- let mut used = HashSet::new();
- validate_pattern(p.post_process(m), &mut used, forbid_expressions);
-}
-
-fn validate_pattern<'a>(
- children: impl Iterator<Item = &'a mut SyntaxNode>,
- used: &mut HashSet<EcoString>,
- forbid_expressions: bool,
-) {
- let mut used_spread = false;
- for child in children {
- match child.kind() {
- SyntaxKind::Ident => {
- if !used.insert(child.text().clone()) {
- child.convert_to_error(
- "at most one binding per identifier is allowed",
- );
- }
- }
- SyntaxKind::Spread => {
- let Some(within) = child.children_mut().last_mut() else { continue };
- if used_spread {
- child.convert_to_error("at most one destructuring sink is allowed");
- continue;
- }
- used_spread = true;
-
- if within.kind() == SyntaxKind::Dots {
- continue;
- } else if forbid_expressions && within.kind() != SyntaxKind::Ident {
- within.convert_to_error(eco_format!(
- "expected identifier, found {}",
- within.kind().name(),
- ));
- child.make_erroneous();
- continue;
- }
-
- if !used.insert(within.text().clone()) {
- within.convert_to_error(
- "at most one binding per identifier is allowed",
- );
- child.make_erroneous();
- }
- }
- SyntaxKind::Named => {
- let Some(within) = child.children_mut().first_mut() else { return };
- if !used.insert(within.text().clone()) {
- within.convert_to_error(
- "at most one binding per identifier is allowed",
- );
- child.make_erroneous();
- }
-
- if forbid_expressions {
- let Some(within) = child.children_mut().last_mut() else { return };
- if within.kind() != SyntaxKind::Ident
- && within.kind() != SyntaxKind::Underscore
- {
- within.convert_to_error(eco_format!(
- "expected identifier, found {}",
- within.kind().name(),
- ));
- child.make_erroneous();
- }
- }
- }
- SyntaxKind::LeftParen
- | SyntaxKind::RightParen
- | SyntaxKind::Comma
- | SyntaxKind::Underscore => {}
- kind => {
- if forbid_expressions {
- child.convert_to_error(eco_format!(
- "expected identifier or destructuring sink, found {}",
- kind.name()
- ));
- }
- }
- }
- }
-}
-
-/// Manages parsing of a stream of tokens.
-struct Parser<'s> {
- text: &'s str,
- lexer: Lexer<'s>,
- prev_end: usize,
- current_start: usize,
- current: SyntaxKind,
- modes: Vec<LexMode>,
- nodes: Vec<SyntaxNode>,
- stop_at_newline: Vec<bool>,
- balanced: bool,
-}
-
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-struct Marker(usize);
-
-impl<'s> Parser<'s> {
- fn new(text: &'s str, offset: usize, mode: LexMode) -> Self {
- let mut lexer = Lexer::new(text, mode);
- lexer.jump(offset);
- let current = lexer.next();
- Self {
- lexer,
- text,
- prev_end: offset,
- current_start: offset,
- current,
- modes: vec![],
- nodes: vec![],
- stop_at_newline: vec![],
- balanced: true,
- }
- }
-
- fn finish(self) -> Vec<SyntaxNode> {
- self.nodes
- }
-
- fn prev_end(&self) -> usize {
- self.prev_end
- }
-
- fn current(&self) -> SyntaxKind {
- self.current
- }
-
- fn current_start(&self) -> usize {
- self.current_start
- }
-
- fn current_end(&self) -> usize {
- self.lexer.cursor()
- }
-
- fn current_text(&self) -> &'s str {
- &self.text[self.current_start..self.current_end()]
- }
-
- fn at(&self, kind: SyntaxKind) -> bool {
- self.current == kind
- }
-
- #[track_caller]
- fn assert(&mut self, kind: SyntaxKind) {
- assert_eq!(self.current, kind);
- self.eat();
- }
-
- fn eof(&self) -> bool {
- self.at(SyntaxKind::Eof)
- }
-
- fn directly_at(&self, kind: SyntaxKind) -> bool {
- self.current == kind && self.prev_end == self.current_start
- }
-
- fn eat_if(&mut self, kind: SyntaxKind) -> bool {
- let at = self.at(kind);
- if at {
- self.eat();
- }
- at
- }
-
- fn convert(&mut self, kind: SyntaxKind) {
- self.current = kind;
- self.eat();
- }
-
- fn newline(&mut self) -> bool {
- self.lexer.newline()
- }
-
- fn column(&self, at: usize) -> usize {
- self.text[..at].chars().rev().take_while(|&c| !is_newline(c)).count()
- }
-
- fn marker(&self) -> Marker {
- Marker(self.nodes.len())
- }
-
- fn node(&self, m: Marker) -> Option<&SyntaxNode> {
- self.nodes.get(m.0)
- }
-
- fn node_mut(&mut self, m: Marker) -> Option<&mut SyntaxNode> {
- self.nodes.get_mut(m.0)
- }
-
- fn post_process(&mut self, m: Marker) -> impl Iterator<Item = &mut SyntaxNode> {
- self.nodes[m.0..]
- .iter_mut()
- .filter(|child| !child.kind().is_error() && !child.kind().is_trivia())
- }
-
- fn wrap(&mut self, m: Marker, kind: SyntaxKind) {
- self.unskip();
- self.wrap_skipless(m, kind);
- self.skip();
- }
-
- fn wrap_skipless(&mut self, m: Marker, kind: SyntaxKind) {
- let from = m.0.min(self.nodes.len());
- let children = self.nodes.drain(from..).collect();
- self.nodes.push(SyntaxNode::inner(kind, children));
- }
-
- fn progress(&self, offset: usize) -> bool {
- offset < self.prev_end
- }
-
- fn enter(&mut self, mode: LexMode) {
- self.modes.push(self.lexer.mode());
- self.lexer.set_mode(mode);
- }
-
- fn exit(&mut self) {
- let mode = self.modes.pop().unwrap();
- if mode != self.lexer.mode() {
- self.unskip();
- self.lexer.set_mode(mode);
- self.lexer.jump(self.current_start);
- self.lex();
- self.skip();
- }
- }
-
- fn stop_at_newline(&mut self, stop: bool) {
- self.stop_at_newline.push(stop);
- }
-
- fn unstop(&mut self) {
- self.unskip();
- self.stop_at_newline.pop();
- self.lexer.jump(self.prev_end);
- self.lex();
- self.skip();
- }
-
- fn eat(&mut self) {
- self.save();
- self.lex();
- self.skip();
- }
-
- fn skip(&mut self) {
- if self.lexer.mode() != LexMode::Markup {
- while self.current.is_trivia() {
- self.save();
- self.lex();
- }
- }
- }
-
- fn unskip(&mut self) {
- if self.lexer.mode() != LexMode::Markup && self.prev_end != self.current_start {
- while self.nodes.last().map_or(false, |last| last.kind().is_trivia()) {
- self.nodes.pop();
- }
-
- self.lexer.jump(self.prev_end);
- self.lex();
- }
- }
-
- fn save(&mut self) {
- let text = self.current_text();
- if self.at(SyntaxKind::Error) {
- let message = self.lexer.take_error().unwrap();
- self.nodes.push(SyntaxNode::error(message, text));
- } else {
- self.nodes.push(SyntaxNode::leaf(self.current, text));
- }
-
- if self.lexer.mode() == LexMode::Markup || !self.current.is_trivia() {
- self.prev_end = self.current_end();
- }
- }
-
- fn lex(&mut self) {
- self.current_start = self.lexer.cursor();
- self.current = self.lexer.next();
- if self.lexer.mode() == LexMode::Code
- && self.lexer.newline()
- && self.stop_at_newline.last().copied().unwrap_or(false)
- && !matches!(self.lexer.clone().next(), SyntaxKind::Else | SyntaxKind::Dot)
- {
- self.current = SyntaxKind::Eof;
- }
- }
-
- fn expect(&mut self, kind: SyntaxKind) -> bool {
- let at = self.at(kind);
- if at {
- self.eat();
- } else {
- self.balanced &= !kind.is_grouping();
- self.expected(kind.name());
- }
- at
- }
-
- fn expect_closing_delimiter(&mut self, open: Marker, kind: SyntaxKind) {
- if !self.eat_if(kind) {
- self.nodes[open.0].convert_to_error("unclosed delimiter");
- }
- }
-
- fn expected(&mut self, thing: &str) {
- self.unskip();
- if self
- .nodes
- .last()
- .map_or(true, |child| child.kind() != SyntaxKind::Error)
- {
- let message = eco_format!("expected {}", thing);
- self.nodes.push(SyntaxNode::error(message, ""));
- }
- self.skip();
- }
-
- fn expected_at(&mut self, m: Marker, thing: &str) {
- let message = eco_format!("expected {}", thing);
- let error = SyntaxNode::error(message, "");
- self.nodes.insert(m.0, error);
- }
-
- fn unexpected(&mut self) {
- self.unskip();
- while self
- .nodes
- .last()
- .map_or(false, |child| child.kind() == SyntaxKind::Error && child.is_empty())
- {
- self.nodes.pop();
- }
- self.skip();
-
- let kind = self.current;
- let offset = self.nodes.len();
- self.eat();
- self.balanced &= !kind.is_grouping();
-
- if !kind.is_error() {
- self.nodes[offset]
- .convert_to_error(eco_format!("unexpected {}", kind.name()));
- }
- }
-}
diff --git a/src/syntax/reparser.rs b/src/syntax/reparser.rs
deleted file mode 100644
index a4186fa7..00000000
--- a/src/syntax/reparser.rs
+++ /dev/null
@@ -1,322 +0,0 @@
-use std::ops::Range;
-
-use super::{
- is_newline, parse, reparse_block, reparse_markup, Span, SyntaxKind, SyntaxNode,
-};
-
-/// Refresh the given syntax node with as little parsing as possible.
-///
-/// Takes the new text, the range in the old text that was replaced and the
-/// length of the replacement and returns the range in the new text that was
-/// ultimately reparsed.
-///
-/// The high-level API for this function is
-/// [`Source::edit`](super::Source::edit).
-pub fn reparse(
- root: &mut SyntaxNode,
- text: &str,
- replaced: Range<usize>,
- replacement_len: usize,
-) -> Range<usize> {
- try_reparse(text, replaced, replacement_len, None, root, 0).unwrap_or_else(|| {
- let id = root.span().id();
- *root = parse(text);
- root.numberize(id, Span::FULL).unwrap();
- 0..text.len()
- })
-}
-
-/// Try to reparse inside the given node.
-fn try_reparse(
- text: &str,
- replaced: Range<usize>,
- replacement_len: usize,
- parent_kind: Option<SyntaxKind>,
- node: &mut SyntaxNode,
- offset: usize,
-) -> Option<Range<usize>> {
- // The range of children which overlap with the edit.
- #[allow(clippy::reversed_empty_ranges)]
- let mut overlap = usize::MAX..0;
- let mut cursor = offset;
- let node_kind = node.kind();
-
- for (i, child) in node.children_mut().iter_mut().enumerate() {
- let prev_range = cursor..cursor + child.len();
- let prev_len = child.len();
- let prev_desc = child.descendants();
-
- // Does the child surround the edit?
- // If so, try to reparse within it or itself.
- if !child.is_leaf() && includes(&prev_range, &replaced) {
- let new_len = prev_len + replacement_len - replaced.len();
- let new_range = cursor..cursor + new_len;
-
- // Try to reparse within the child.
- if let Some(range) = try_reparse(
- text,
- replaced.clone(),
- replacement_len,
- Some(node_kind),
- child,
- cursor,
- ) {
- assert_eq!(child.len(), new_len);
- let new_desc = child.descendants();
- node.update_parent(prev_len, new_len, prev_desc, new_desc);
- return Some(range);
- }
-
- // If the child is a block, try to reparse the block.
- if child.kind().is_block() {
- if let Some(newborn) = reparse_block(text, new_range.clone()) {
- return node
- .replace_children(i..i + 1, vec![newborn])
- .is_ok()
- .then_some(new_range);
- }
- }
- }
-
- // Does the child overlap with the edit?
- if overlaps(&prev_range, &replaced) {
- overlap.start = overlap.start.min(i);
- overlap.end = i + 1;
- }
-
- // Is the child beyond the edit?
- if replaced.end < cursor {
- break;
- }
-
- cursor += child.len();
- }
-
- // Try to reparse a range of markup expressions within markup. This is only
- // possible if the markup is top-level or contained in a block, not if it is
- // contained in things like headings or lists because too much can go wrong
- // with indent and line breaks.
- if overlap.is_empty()
- || node.kind() != SyntaxKind::Markup
- || !matches!(parent_kind, None | Some(SyntaxKind::ContentBlock))
- {
- return None;
- }
-
- let children = node.children_mut();
-
- // Reparse a segment. Retries until it works, taking exponentially more
- // children into account.
- let mut expansion = 1;
- loop {
- // Add slack in both directions.
- let mut start = overlap.start.saturating_sub(expansion.max(2));
- let mut end = (overlap.end + expansion).min(children.len());
-
- // Expand to the left.
- while start > 0 && expand(&children[start]) {
- start -= 1;
- }
-
- // Expand to the right.
- while end < children.len() && expand(&children[end]) {
- end += 1;
- }
-
- // Also take hashtag.
- if start > 0 && children[start - 1].kind() == SyntaxKind::Hashtag {
- start -= 1;
- }
-
- // Synthesize what `at_start` and `nesting` would be at the start of the
- // reparse.
- let mut prefix_len = 0;
- let mut nesting = 0;
- let mut at_start = true;
- for child in &children[..start] {
- prefix_len += child.len();
- next_at_start(child, &mut at_start);
- next_nesting(child, &mut nesting);
- }
-
- // Determine what `at_start` will have to be at the end of the reparse.
- let mut prev_len = 0;
- let mut prev_at_start_after = at_start;
- let mut prev_nesting_after = nesting;
- for child in &children[start..end] {
- prev_len += child.len();
- next_at_start(child, &mut prev_at_start_after);
- next_nesting(child, &mut prev_nesting_after);
- }
-
- // Determine the range in the new text that we want to reparse.
- let shifted = offset + prefix_len;
- let new_len = prev_len + replacement_len - replaced.len();
- let new_range = shifted..shifted + new_len;
- let at_end = end == children.len();
-
- // Stop parsing early if this kind is encountered.
- let stop_kind = match parent_kind {
- Some(_) => SyntaxKind::RightBracket,
- None => SyntaxKind::Eof,
- };
-
- // Reparse!
- let reparsed = reparse_markup(
- text,
- new_range.clone(),
- &mut at_start,
- &mut nesting,
- |kind| kind == stop_kind,
- );
-
- if let Some(newborns) = reparsed {
- // If more children follow, at_start must match its previous value.
- // Similarly, if we children follow or we not top-level the nesting
- // must match its previous value.
- if (at_end || at_start == prev_at_start_after)
- && ((at_end && parent_kind.is_none()) || nesting == prev_nesting_after)
- {
- return node
- .replace_children(start..end, newborns)
- .is_ok()
- .then_some(new_range);
- }
- }
-
- // If it didn't even work with all children, we give up.
- if start == 0 && at_end {
- break;
- }
-
- // Exponential expansion to both sides.
- expansion *= 2;
- }
-
- None
-}
-
-/// Whether the inner range is fully contained in the outer one (no touching).
-fn includes(outer: &Range<usize>, inner: &Range<usize>) -> bool {
- outer.start < inner.start && outer.end > inner.end
-}
-
-/// Whether the first and second range overlap or touch.
-fn overlaps(first: &Range<usize>, second: &Range<usize>) -> bool {
- (first.start <= second.start && second.start <= first.end)
- || (second.start <= first.start && first.start <= second.end)
-}
-
-/// Whether the selection should be expanded beyond a node of this kind.
-fn expand(node: &SyntaxNode) -> bool {
- let kind = node.kind();
- kind.is_trivia()
- || kind.is_error()
- || kind == SyntaxKind::Semicolon
- || node.text() == "/"
- || node.text() == ":"
-}
-
-/// Whether `at_start` would still be true after this node given the
-/// previous value of the property.
-fn next_at_start(node: &SyntaxNode, at_start: &mut bool) {
- let kind = node.kind();
- if kind.is_trivia() {
- *at_start |= kind == SyntaxKind::Parbreak
- || (kind == SyntaxKind::Space && node.text().chars().any(is_newline));
- } else {
- *at_start = false;
- }
-}
-
-/// Update `nesting` based on the node.
-fn next_nesting(node: &SyntaxNode, nesting: &mut usize) {
- if node.kind() == SyntaxKind::Text {
- match node.text().as_str() {
- "[" => *nesting += 1,
- "]" if *nesting > 0 => *nesting -= 1,
- _ => {}
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::ops::Range;
-
- use super::super::{parse, Source, Span};
-
- #[track_caller]
- fn test(prev: &str, range: Range<usize>, with: &str, incremental: bool) {
- let mut source = Source::detached(prev);
- let prev = source.root().clone();
- let range = source.edit(range, with);
- let mut found = source.root().clone();
- let mut expected = parse(source.text());
- found.synthesize(Span::detached());
- expected.synthesize(Span::detached());
- if found != expected {
- eprintln!("source: {:?}", source.text());
- eprintln!("previous: {prev:#?}");
- eprintln!("expected: {expected:#?}");
- eprintln!("found: {found:#?}");
- panic!("test failed");
- }
- if incremental {
- assert_ne!(source.len_bytes(), range.len(), "should have been incremental");
- } else {
- assert_eq!(
- source.len_bytes(),
- range.len(),
- "shouldn't have been incremental"
- );
- }
- }
-
- #[test]
- fn test_reparse_markup() {
- test("abc~def~gh~", 5..6, "+", true);
- test("~~~~~~~", 3..4, "A", true);
- test("abc~~", 1..2, "", true);
- test("#var. hello", 5..6, " ", false);
- test("#var;hello", 9..10, "a", false);
- test("https:/world", 7..7, "/", false);
- test("hello world", 7..12, "walkers", false);
- test("some content", 0..12, "", false);
- test("", 0..0, "do it", false);
- test("a d e", 1..3, " b c d", false);
- test("~*~*~", 2..2, "*", false);
- test("::1\n2. a\n3", 7..7, "4", true);
- test("* #{1+2} *", 6..7, "3", true);
- test("#{(0, 1, 2)}", 6..7, "11pt", true);
- test("\n= A heading", 4..4, "n evocative", false);
- test("#call() abc~d", 7..7, "[]", true);
- test("a your thing a", 6..7, "a", false);
- test("#grid(columns: (auto, 1fr, 40%))", 16..20, "4pt", false);
- test("abc\n= a heading\njoke", 3..4, "\nmore\n\n", true);
- test("#show f: a => b..", 16..16, "c", false);
- test("#for", 4..4, "//", false);
- test("a\n#let \nb", 7..7, "i", true);
- test(r"#{{let x = z}; a = 1} b", 7..7, "//", false);
- test(r#"a ```typst hello```"#, 16..17, "", false);
- }
-
- #[test]
- fn test_reparse_block() {
- test("Hello #{ x + 1 }!", 9..10, "abc", true);
- test("A#{}!", 3..3, "\"", false);
- test("#{ [= x] }!", 5..5, "=", true);
- test("#[[]]", 3..3, "\\", true);
- test("#[[ab]]", 4..5, "\\", true);
- test("#{}}", 2..2, "{", false);
- test("A: #[BC]", 6..6, "{", true);
- test("A: #[BC]", 6..6, "#{", true);
- test("A: #[BC]", 6..6, "#{}", true);
- test("#{\"ab\"}A", 5..5, "c", true);
- test("#{\"ab\"}A", 5..6, "c", false);
- test("a#[]b", 3..3, "#{", true);
- test("a#{call(); abc}b", 8..8, "[]", true);
- test("a #while x {\n g(x) \n} b", 12..12, "//", true);
- test("a#[]b", 3..3, "[hey]", true);
- }
-}
diff --git a/src/syntax/source.rs b/src/syntax/source.rs
deleted file mode 100644
index 2f3e4144..00000000
--- a/src/syntax/source.rs
+++ /dev/null
@@ -1,421 +0,0 @@
-//! Source file management.
-
-use std::fmt::{self, Debug, Formatter};
-use std::hash::{Hash, Hasher};
-use std::ops::Range;
-use std::sync::Arc;
-
-use comemo::Prehashed;
-
-use super::ast::Markup;
-use super::reparser::reparse;
-use super::{is_newline, parse, LinkedNode, Span, SyntaxNode};
-use crate::diag::SourceResult;
-use crate::file::FileId;
-use crate::util::StrExt;
-
-/// A source file.
-///
-/// All line and column indices start at zero, just like byte indices. Only for
-/// user-facing display, you should add 1 to them.
-///
-/// Values of this type are cheap to clone and hash.
-#[derive(Clone)]
-pub struct Source(Arc<Repr>);
-
-/// The internal representation.
-#[derive(Clone)]
-struct Repr {
- id: FileId,
- text: Prehashed<String>,
- root: Prehashed<SyntaxNode>,
- lines: Vec<Line>,
-}
-
-impl Source {
- /// Create a new source file.
- #[tracing::instrument(skip_all)]
- pub fn new(id: FileId, text: String) -> Self {
- let mut root = parse(&text);
- root.numberize(id, Span::FULL).unwrap();
- Self(Arc::new(Repr {
- id,
- lines: lines(&text),
- text: Prehashed::new(text),
- root: Prehashed::new(root),
- }))
- }
-
- /// Create a source file without a real id and path, usually for testing.
- pub fn detached(text: impl Into<String>) -> Self {
- Self::new(FileId::detached(), text.into())
- }
-
- /// Create a source file with the same synthetic span for all nodes.
- pub fn synthesized(text: String, span: Span) -> Self {
- let mut root = parse(&text);
- root.synthesize(span);
- Self(Arc::new(Repr {
- id: FileId::detached(),
- lines: lines(&text),
- text: Prehashed::new(text),
- root: Prehashed::new(root),
- }))
- }
-
- /// The root node of the file's untyped syntax tree.
- pub fn root(&self) -> &SyntaxNode {
- &self.0.root
- }
-
- /// The root node of the file's typed abstract syntax tree.
- pub fn ast(&self) -> SourceResult<Markup> {
- let errors = self.root().errors();
- if errors.is_empty() {
- Ok(self.root().cast().expect("root node must be markup"))
- } else {
- Err(Box::new(errors))
- }
- }
-
- /// The id of the source file.
- pub fn id(&self) -> FileId {
- self.0.id
- }
-
- /// The whole source as a string slice.
- pub fn text(&self) -> &str {
- &self.0.text
- }
-
- /// Slice out the part of the source code enclosed by the range.
- pub fn get(&self, range: Range<usize>) -> Option<&str> {
- self.text().get(range)
- }
-
- /// Fully replace the source text.
- pub fn replace(&mut self, text: String) {
- let inner = Arc::make_mut(&mut self.0);
- inner.text = Prehashed::new(text);
- inner.lines = lines(&inner.text);
- let mut root = parse(&inner.text);
- root.numberize(inner.id, Span::FULL).unwrap();
- inner.root = Prehashed::new(root);
- }
-
- /// Edit the source file by replacing the given range.
- ///
- /// Returns the range in the new source that was ultimately reparsed.
- ///
- /// The method panics if the `replace` range is out of bounds.
- #[track_caller]
- pub fn edit(&mut self, replace: Range<usize>, with: &str) -> Range<usize> {
- let start_byte = replace.start;
- let start_utf16 = self.byte_to_utf16(start_byte).unwrap();
- let line = self.byte_to_line(start_byte).unwrap();
-
- let inner = Arc::make_mut(&mut self.0);
-
- // Update the text itself.
- inner.text.update(|text| text.replace_range(replace.clone(), with));
-
- // Remove invalidated line starts.
- inner.lines.truncate(line + 1);
-
- // Handle adjoining of \r and \n.
- if inner.text[..start_byte].ends_with('\r') && with.starts_with('\n') {
- inner.lines.pop();
- }
-
- // Recalculate the line starts after the edit.
- inner.lines.extend(lines_from(
- start_byte,
- start_utf16,
- &inner.text[start_byte..],
- ));
-
- // Incrementally reparse the replaced range.
- inner
- .root
- .update(|root| reparse(root, &inner.text, replace, with.len()))
- }
-
- /// Get the length of the file in UTF-8 encoded bytes.
- pub fn len_bytes(&self) -> usize {
- self.text().len()
- }
-
- /// Get the length of the file in UTF-16 code units.
- pub fn len_utf16(&self) -> usize {
- let last = self.0.lines.last().unwrap();
- last.utf16_idx + self.0.text[last.byte_idx..].len_utf16()
- }
-
- /// Get the length of the file in lines.
- pub fn len_lines(&self) -> usize {
- self.0.lines.len()
- }
-
- /// Find the node with the given span.
- ///
- /// Returns `None` if the span does not point into this source file.
- pub fn find(&self, span: Span) -> Option<LinkedNode<'_>> {
- LinkedNode::new(self.root()).find(span)
- }
-
- /// Return the index of the UTF-16 code unit at the byte index.
- pub fn byte_to_utf16(&self, byte_idx: usize) -> Option<usize> {
- let line_idx = self.byte_to_line(byte_idx)?;
- let line = self.0.lines.get(line_idx)?;
- let head = self.0.text.get(line.byte_idx..byte_idx)?;
- Some(line.utf16_idx + head.len_utf16())
- }
-
- /// Return the index of the line that contains the given byte index.
- pub fn byte_to_line(&self, byte_idx: usize) -> Option<usize> {
- (byte_idx <= self.0.text.len()).then(|| {
- match self.0.lines.binary_search_by_key(&byte_idx, |line| line.byte_idx) {
- Ok(i) => i,
- Err(i) => i - 1,
- }
- })
- }
-
- /// Return the index of the column at the byte index.
- ///
- /// The column is defined as the number of characters in the line before the
- /// byte index.
- pub fn byte_to_column(&self, byte_idx: usize) -> Option<usize> {
- let line = self.byte_to_line(byte_idx)?;
- let start = self.line_to_byte(line)?;
- let head = self.get(start..byte_idx)?;
- Some(head.chars().count())
- }
-
- /// Return the byte index at the UTF-16 code unit.
- pub fn utf16_to_byte(&self, utf16_idx: usize) -> Option<usize> {
- let line = self.0.lines.get(
- match self.0.lines.binary_search_by_key(&utf16_idx, |line| line.utf16_idx) {
- Ok(i) => i,
- Err(i) => i - 1,
- },
- )?;
-
- let mut k = line.utf16_idx;
- for (i, c) in self.0.text[line.byte_idx..].char_indices() {
- if k >= utf16_idx {
- return Some(line.byte_idx + i);
- }
- k += c.len_utf16();
- }
-
- (k == utf16_idx).then_some(self.0.text.len())
- }
-
- /// Return the byte position at which the given line starts.
- pub fn line_to_byte(&self, line_idx: usize) -> Option<usize> {
- self.0.lines.get(line_idx).map(|line| line.byte_idx)
- }
-
- /// Return the range which encloses the given line.
- pub fn line_to_range(&self, line_idx: usize) -> Option<Range<usize>> {
- let start = self.line_to_byte(line_idx)?;
- let end = self.line_to_byte(line_idx + 1).unwrap_or(self.0.text.len());
- Some(start..end)
- }
-
- /// Return the byte index of the given (line, column) pair.
- ///
- /// The column defines the number of characters to go beyond the start of
- /// the line.
- pub fn line_column_to_byte(
- &self,
- line_idx: usize,
- column_idx: usize,
- ) -> Option<usize> {
- let range = self.line_to_range(line_idx)?;
- let line = self.get(range.clone())?;
- let mut chars = line.chars();
- for _ in 0..column_idx {
- chars.next();
- }
- Some(range.start + (line.len() - chars.as_str().len()))
- }
-}
-
-impl Debug for Source {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "Source({})", self.id().path().display())
- }
-}
-
-impl Hash for Source {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.0.id.hash(state);
- self.0.text.hash(state);
- self.0.root.hash(state);
- }
-}
-
-impl AsRef<str> for Source {
- fn as_ref(&self) -> &str {
- self.text()
- }
-}
-
-/// Metadata about a line.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-struct Line {
- /// The UTF-8 byte offset where the line starts.
- byte_idx: usize,
- /// The UTF-16 codepoint offset where the line starts.
- utf16_idx: usize,
-}
-
-/// Create a line vector.
-fn lines(text: &str) -> Vec<Line> {
- std::iter::once(Line { byte_idx: 0, utf16_idx: 0 })
- .chain(lines_from(0, 0, text))
- .collect()
-}
-
-/// Compute a line iterator from an offset.
-fn lines_from(
- byte_offset: usize,
- utf16_offset: usize,
- text: &str,
-) -> impl Iterator<Item = Line> + '_ {
- let mut s = unscanny::Scanner::new(text);
- let mut utf16_idx = utf16_offset;
-
- std::iter::from_fn(move || {
- s.eat_until(|c: char| {
- utf16_idx += c.len_utf16();
- is_newline(c)
- });
-
- if s.done() {
- return None;
- }
-
- if s.eat() == Some('\r') && s.eat_if('\n') {
- utf16_idx += 1;
- }
-
- Some(Line { byte_idx: byte_offset + s.cursor(), utf16_idx })
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- const TEST: &str = "ä\tcde\nf💛g\r\nhi\rjkl";
-
- #[test]
- fn test_source_file_new() {
- let source = Source::detached(TEST);
- assert_eq!(
- source.0.lines,
- [
- Line { byte_idx: 0, utf16_idx: 0 },
- Line { byte_idx: 7, utf16_idx: 6 },
- Line { byte_idx: 15, utf16_idx: 12 },
- Line { byte_idx: 18, utf16_idx: 15 },
- ]
- );
- }
-
- #[test]
- fn test_source_file_pos_to_line() {
- let source = Source::detached(TEST);
- assert_eq!(source.byte_to_line(0), Some(0));
- assert_eq!(source.byte_to_line(2), Some(0));
- assert_eq!(source.byte_to_line(6), Some(0));
- assert_eq!(source.byte_to_line(7), Some(1));
- assert_eq!(source.byte_to_line(8), Some(1));
- assert_eq!(source.byte_to_line(12), Some(1));
- assert_eq!(source.byte_to_line(21), Some(3));
- assert_eq!(source.byte_to_line(22), None);
- }
-
- #[test]
- fn test_source_file_pos_to_column() {
- let source = Source::detached(TEST);
- assert_eq!(source.byte_to_column(0), Some(0));
- assert_eq!(source.byte_to_column(2), Some(1));
- assert_eq!(source.byte_to_column(6), Some(5));
- assert_eq!(source.byte_to_column(7), Some(0));
- assert_eq!(source.byte_to_column(8), Some(1));
- assert_eq!(source.byte_to_column(12), Some(2));
- }
-
- #[test]
- fn test_source_file_utf16() {
- #[track_caller]
- fn roundtrip(source: &Source, byte_idx: usize, utf16_idx: usize) {
- let middle = source.byte_to_utf16(byte_idx).unwrap();
- let result = source.utf16_to_byte(middle).unwrap();
- assert_eq!(middle, utf16_idx);
- assert_eq!(result, byte_idx);
- }
-
- let source = Source::detached(TEST);
- roundtrip(&source, 0, 0);
- roundtrip(&source, 2, 1);
- roundtrip(&source, 3, 2);
- roundtrip(&source, 8, 7);
- roundtrip(&source, 12, 9);
- roundtrip(&source, 21, 18);
- assert_eq!(source.byte_to_utf16(22), None);
- assert_eq!(source.utf16_to_byte(19), None);
- }
-
- #[test]
- fn test_source_file_roundtrip() {
- #[track_caller]
- fn roundtrip(source: &Source, byte_idx: usize) {
- let line = source.byte_to_line(byte_idx).unwrap();
- let column = source.byte_to_column(byte_idx).unwrap();
- let result = source.line_column_to_byte(line, column).unwrap();
- assert_eq!(result, byte_idx);
- }
-
- let source = Source::detached(TEST);
- roundtrip(&source, 0);
- roundtrip(&source, 7);
- roundtrip(&source, 12);
- roundtrip(&source, 21);
- }
-
- #[test]
- fn test_source_file_edit() {
- // This tests only the non-parser parts. The reparsing itself is
- // tested separately.
- #[track_caller]
- fn test(prev: &str, range: Range<usize>, with: &str, after: &str) {
- let mut source = Source::detached(prev);
- let result = Source::detached(after);
- source.edit(range, with);
- assert_eq!(source.text(), result.text());
- assert_eq!(source.0.lines, result.0.lines);
- }
-
- // Test inserting at the beginning.
- test("abc\n", 0..0, "hi\n", "hi\nabc\n");
- test("\nabc", 0..0, "hi\r", "hi\r\nabc");
-
- // Test editing in the middle.
- test(TEST, 4..16, "❌", "ä\tc❌i\rjkl");
-
- // Test appending.
- test("abc\ndef", 7..7, "hi", "abc\ndefhi");
- test("abc\ndef\n", 8..8, "hi", "abc\ndef\nhi");
-
- // Test appending with adjoining \r and \n.
- test("abc\ndef\r", 8..8, "\nghi", "abc\ndef\r\nghi");
-
- // Test removing everything.
- test(TEST, 0..21, "", "");
- }
-}
diff --git a/src/syntax/span.rs b/src/syntax/span.rs
deleted file mode 100644
index 5c220252..00000000
--- a/src/syntax/span.rs
+++ /dev/null
@@ -1,148 +0,0 @@
-use std::fmt::{self, Debug, Formatter};
-use std::num::NonZeroU64;
-use std::ops::Range;
-
-use super::Source;
-use crate::file::FileId;
-use crate::World;
-
-/// A unique identifier for a syntax node.
-///
-/// This is used throughout the compiler to track which source section an error
-/// or element stems from. Can be [mapped back](Self::range) to a byte range for
-/// user facing display.
-///
-/// During editing, the span values stay mostly stable, even for nodes behind an
-/// insertion. This is not true for simple ranges as they would shift. Spans can
-/// be used as inputs to memoized functions without hurting cache performance
-/// when text is inserted somewhere in the document other than the end.
-///
-/// Span ids are ordered in the syntax tree to enable quickly finding the node
-/// with some id:
-/// - The id of a parent is always smaller than the ids of any of its children.
-/// - The id of a node is always greater than any id in the subtrees of any left
-/// sibling and smaller than any id in the subtrees of any right sibling.
-///
-/// This type takes up 8 bytes and is null-optimized (i.e. `Option<Span>` also
-/// takes 8 bytes).
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Span(NonZeroU64);
-
-impl Span {
- /// The full range of numbers available for span numbering.
- pub const FULL: Range<u64> = 2..(1 << Self::BITS);
- const DETACHED: u64 = 1;
-
- // Data layout:
- // | 16 bits source id | 48 bits number |
- const BITS: usize = 48;
-
- /// Create a new span from a source id and a unique number.
- ///
- /// Panics if the `number` is not contained in `FULL`.
- #[track_caller]
- pub const fn new(id: FileId, number: u64) -> Self {
- assert!(
- Self::FULL.start <= number && number < Self::FULL.end,
- "span number outside valid range"
- );
-
- Self::pack(id, number)
- }
-
- /// A span that does not point into any source file.
- pub const fn detached() -> Self {
- Self::pack(FileId::detached(), Self::DETACHED)
- }
-
- /// Pack the components into a span.
- #[track_caller]
- const fn pack(id: FileId, number: u64) -> Span {
- let bits = ((id.as_u16() as u64) << Self::BITS) | number;
- match NonZeroU64::new(bits) {
- Some(v) => Self(v),
- None => panic!("span encoding is zero"),
- }
- }
-
- /// The id of the source file the span points into.
- pub const fn id(self) -> FileId {
- FileId::from_u16((self.0.get() >> Self::BITS) as u16)
- }
-
- /// The unique number of the span within its source file.
- pub const fn number(self) -> u64 {
- self.0.get() & ((1 << Self::BITS) - 1)
- }
-
- /// Whether the span is detached.
- pub const fn is_detached(self) -> bool {
- self.id().is_detached()
- }
-
- /// Get the byte range for this span.
- #[track_caller]
- pub fn range(self, world: &dyn World) -> Range<usize> {
- let source = world
- .source(self.id())
- .expect("span does not point into any source file");
- self.range_in(&source)
- }
-
- /// Get the byte range for this span in the given source file.
- #[track_caller]
- pub fn range_in(self, source: &Source) -> Range<usize> {
- source
- .find(self)
- .expect("span does not point into this source file")
- .range()
- }
-}
-
-/// A value with a span locating it in the source code.
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Spanned<T> {
- /// The spanned value.
- pub v: T,
- /// The value's location in source code.
- pub span: Span,
-}
-
-impl<T> Spanned<T> {
- /// Create a new instance from a value and its span.
- pub fn new(v: T, span: Span) -> Self {
- Self { v, span }
- }
-
- /// Convert from `&Spanned<T>` to `Spanned<&T>`
- pub fn as_ref(&self) -> Spanned<&T> {
- Spanned { v: &self.v, span: self.span }
- }
-
- /// Map the value using a function.
- pub fn map<F, U>(self, f: F) -> Spanned<U>
- where
- F: FnOnce(T) -> U,
- {
- Spanned { v: f(self.v), span: self.span }
- }
-}
-
-impl<T: Debug> Debug for Spanned<T> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.v.fmt(f)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{FileId, Span};
-
- #[test]
- fn test_span_encoding() {
- let id = FileId::from_u16(5);
- let span = Span::new(id, 10);
- assert_eq!(span.id(), id);
- assert_eq!(span.number(), 10);
- }
-}
diff --git a/src/util/bytes.rs b/src/util/bytes.rs
deleted file mode 100644
index 9165467b..00000000
--- a/src/util/bytes.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use std::borrow::Cow;
-use std::fmt::{self, Debug, Formatter};
-use std::ops::Deref;
-use std::sync::Arc;
-
-use comemo::Prehashed;
-
-/// A shared byte buffer that is cheap to clone and hash.
-#[derive(Clone, Hash, Eq, PartialEq)]
-pub struct Bytes(Arc<Prehashed<Cow<'static, [u8]>>>);
-
-impl Bytes {
- /// Create a buffer from a static byte slice.
- pub fn from_static(slice: &'static [u8]) -> Self {
- Self(Arc::new(Prehashed::new(Cow::Borrowed(slice))))
- }
-
- /// Return a view into the buffer.
- pub fn as_slice(&self) -> &[u8] {
- self
- }
-
- /// Return a copy of the buffer as a vector.
- pub fn to_vec(&self) -> Vec<u8> {
- self.0.to_vec()
- }
-}
-
-impl From<&[u8]> for Bytes {
- fn from(slice: &[u8]) -> Self {
- Self(Arc::new(Prehashed::new(slice.to_vec().into())))
- }
-}
-
-impl From<Vec<u8>> for Bytes {
- fn from(vec: Vec<u8>) -> Self {
- Self(Arc::new(Prehashed::new(vec.into())))
- }
-}
-
-impl Deref for Bytes {
- type Target = [u8];
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl AsRef<[u8]> for Bytes {
- fn as_ref(&self) -> &[u8] {
- self
- }
-}
-
-impl Debug for Bytes {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "bytes({})", self.len())
- }
-}
diff --git a/src/util/fat.rs b/src/util/fat.rs
deleted file mode 100644
index d3c9bb20..00000000
--- a/src/util/fat.rs
+++ /dev/null
@@ -1,55 +0,0 @@
-//! Fat pointer handling.
-//!
-//! This assumes the memory representation of fat pointers. Although it is not
-//! guaranteed by Rust, it's improbable that it will change. Still, when the
-//! pointer metadata APIs are stable, we should definitely move to them:
-//! <https://github.com/rust-lang/rust/issues/81513>
-
-use std::alloc::Layout;
-use std::mem;
-
-/// Create a fat pointer from a data address and a vtable address.
-///
-/// # Safety
-/// Must only be called when `T` is a `dyn Trait`. The data address must point
-/// to a value whose type implements the trait of `T` and the `vtable` must have
-/// been extracted with [`vtable`].
-#[track_caller]
-pub unsafe fn from_raw_parts<T: ?Sized>(data: *const (), vtable: *const ()) -> *const T {
- let fat = FatPointer { data, vtable };
- debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>());
- mem::transmute_copy::<FatPointer, *const T>(&fat)
-}
-
-/// Create a mutable fat pointer from a data address and a vtable address.
-///
-/// # Safety
-/// Must only be called when `T` is a `dyn Trait`. The data address must point
-/// to a value whose type implements the trait of `T` and the `vtable` must have
-/// been extracted with [`vtable`].
-#[track_caller]
-pub unsafe fn from_raw_parts_mut<T: ?Sized>(data: *mut (), vtable: *const ()) -> *mut T {
- let fat = FatPointer { data, vtable };
- debug_assert_eq!(Layout::new::<*mut T>(), Layout::new::<FatPointer>());
- mem::transmute_copy::<FatPointer, *mut T>(&fat)
-}
-
-/// Extract the address to a trait object's vtable.
-///
-/// # Safety
-/// Must only be called when `T` is a `dyn Trait`.
-#[track_caller]
-pub unsafe fn vtable<T: ?Sized>(ptr: *const T) -> *const () {
- debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>());
- mem::transmute_copy::<*const T, FatPointer>(&ptr).vtable
-}
-
-/// The memory representation of a trait object pointer.
-///
-/// Although this is not guaranteed by Rust, it's improbable that it will
-/// change.
-#[repr(C)]
-struct FatPointer {
- data: *const (),
- vtable: *const (),
-}
diff --git a/src/util/mod.rs b/src/util/mod.rs
deleted file mode 100644
index 05914b04..00000000
--- a/src/util/mod.rs
+++ /dev/null
@@ -1,268 +0,0 @@
-//! Utilities.
-
-pub mod fat;
-
-mod bytes;
-
-pub use bytes::Bytes;
-
-use std::fmt::{self, Debug, Formatter};
-use std::hash::Hash;
-use std::num::NonZeroUsize;
-use std::path::{Component, Path, PathBuf};
-use std::sync::Arc;
-
-use siphasher::sip128::{Hasher128, SipHasher13};
-
-/// Turn a closure into a struct implementing [`Debug`].
-pub fn debug<F>(f: F) -> impl Debug
-where
- F: Fn(&mut Formatter) -> fmt::Result,
-{
- struct Wrapper<F>(F);
-
- impl<F> Debug for Wrapper<F>
- where
- F: Fn(&mut Formatter) -> fmt::Result,
- {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- self.0(f)
- }
- }
-
- Wrapper(f)
-}
-
-/// Calculate a 128-bit siphash of a value.
-pub fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
- let mut state = SipHasher13::new();
- value.hash(&mut state);
- state.finish128().as_u128()
-}
-
-/// An extra constant for [`NonZeroUsize`].
-pub trait NonZeroExt {
- /// The number `1`.
- const ONE: Self;
-}
-
-impl NonZeroExt for NonZeroUsize {
- const ONE: Self = match Self::new(1) {
- Some(v) => v,
- None => unreachable!(),
- };
-}
-
-/// Extra methods for [`str`].
-pub trait StrExt {
- /// The number of code units this string would use if it was encoded in
- /// UTF16. This runs in linear time.
- fn len_utf16(&self) -> usize;
-}
-
-impl StrExt for str {
- fn len_utf16(&self) -> usize {
- self.chars().map(char::len_utf16).sum()
- }
-}
-
-/// Extra methods for [`Arc`].
-pub trait ArcExt<T> {
- /// Takes the inner value if there is exactly one strong reference and
- /// clones it otherwise.
- fn take(self) -> T;
-}
-
-impl<T: Clone> ArcExt<T> for Arc<T> {
- fn take(self) -> T {
- match Arc::try_unwrap(self) {
- Ok(v) => v,
- Err(rc) => (*rc).clone(),
- }
- }
-}
-
-/// Extra methods for [`[T]`](slice).
-pub trait SliceExt<T> {
- /// Split a slice into consecutive runs with the same key and yield for
- /// each such run the key and the slice of elements with that key.
- fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F>
- where
- F: FnMut(&T) -> K,
- K: PartialEq;
-}
-
-impl<T> SliceExt<T> for [T] {
- fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F> {
- GroupByKey { slice: self, f }
- }
-}
-
-/// This struct is created by [`SliceExt::group_by_key`].
-pub struct GroupByKey<'a, T, F> {
- slice: &'a [T],
- f: F,
-}
-
-impl<'a, T, K, F> Iterator for GroupByKey<'a, T, F>
-where
- F: FnMut(&T) -> K,
- K: PartialEq,
-{
- type Item = (K, &'a [T]);
-
- fn next(&mut self) -> Option<Self::Item> {
- let mut iter = self.slice.iter();
- let key = (self.f)(iter.next()?);
- let count = 1 + iter.take_while(|t| (self.f)(t) == key).count();
- let (head, tail) = self.slice.split_at(count);
- self.slice = tail;
- Some((key, head))
- }
-}
-
-/// Extra methods for [`Path`].
-pub trait PathExt {
- /// Lexically normalize a path.
- fn normalize(&self) -> PathBuf;
-
- /// Treat `self` as a virtual root relative to which the `path` is resolved.
- ///
- /// Returns `None` if the path lexically escapes the root. The path
- /// might still escape through symlinks.
- fn join_rooted(&self, path: &Path) -> Option<PathBuf>;
-}
-
-impl PathExt for Path {
- fn normalize(&self) -> PathBuf {
- let mut out = PathBuf::new();
- for component in self.components() {
- match component {
- Component::CurDir => {}
- Component::ParentDir => match out.components().next_back() {
- Some(Component::Normal(_)) => {
- out.pop();
- }
- _ => out.push(component),
- },
- Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
- out.push(component)
- }
- }
- }
- if out.as_os_str().is_empty() {
- out.push(Component::CurDir);
- }
- out
- }
-
- fn join_rooted(&self, path: &Path) -> Option<PathBuf> {
- let mut parts: Vec<_> = self.components().collect();
- let root = parts.len();
- for component in path.components() {
- match component {
- Component::Prefix(_) => return None,
- Component::RootDir => parts.truncate(root),
- Component::CurDir => {}
- Component::ParentDir => {
- if parts.len() <= root {
- return None;
- }
- parts.pop();
- }
- Component::Normal(_) => parts.push(component),
- }
- }
- if parts.len() < root {
- return None;
- }
- Some(parts.into_iter().collect())
- }
-}
-
-/// Format pieces separated with commas and a final "and" or "or".
-pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String {
- let mut buf = String::new();
- for (i, part) in pieces.iter().enumerate() {
- match i {
- 0 => {}
- 1 if pieces.len() == 2 => {
- buf.push(' ');
- buf.push_str(last);
- buf.push(' ');
- }
- i if i + 1 == pieces.len() => {
- buf.push_str(", ");
- buf.push_str(last);
- buf.push(' ');
- }
- _ => buf.push_str(", "),
- }
- buf.push_str(part.as_ref());
- }
- buf
-}
-
-/// Format a comma-separated list.
-///
-/// Tries to format horizontally, but falls back to vertical formatting if the
-/// pieces are too long.
-pub fn pretty_comma_list(pieces: &[impl AsRef<str>], trailing_comma: bool) -> String {
- const MAX_WIDTH: usize = 50;
-
- let mut buf = String::new();
- let len = pieces.iter().map(|s| s.as_ref().len()).sum::<usize>()
- + 2 * pieces.len().saturating_sub(1);
-
- if len <= MAX_WIDTH {
- for (i, piece) in pieces.iter().enumerate() {
- if i > 0 {
- buf.push_str(", ");
- }
- buf.push_str(piece.as_ref());
- }
- if trailing_comma {
- buf.push(',');
- }
- } else {
- for piece in pieces {
- buf.push_str(piece.as_ref().trim());
- buf.push_str(",\n");
- }
- }
-
- buf
-}
-
-/// Format an array-like construct.
-///
-/// Tries to format horizontally, but falls back to vertical formatting if the
-/// pieces are too long.
-pub fn pretty_array_like(parts: &[impl AsRef<str>], trailing_comma: bool) -> String {
- let list = pretty_comma_list(parts, trailing_comma);
- let mut buf = String::new();
- buf.push('(');
- if list.contains('\n') {
- buf.push('\n');
- for (i, line) in list.lines().enumerate() {
- if i > 0 {
- buf.push('\n');
- }
- buf.push_str(" ");
- buf.push_str(line);
- }
- buf.push('\n');
- } else {
- buf.push_str(&list);
- }
- buf.push(')');
- buf
-}
-
-/// Check if the [`Option`]-wrapped L is same to R.
-pub fn option_eq<L, R>(left: Option<L>, other: R) -> bool
-where
- L: PartialEq<R>,
-{
- left.map(|v| v == other).unwrap_or(false)
-}